From 825469a7f21f49b2fa3aab6e220a011ee652323b Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 6 Nov 2026 13:28:00 +0100 Subject: [PATCH] feat(native): share a booking document to TREK and import it into a trip TREK now appears under "Open in" and in the share sheet for PDFs, calendar entries, Wallet passes and mails (iOS document types, Android VIEW and SEND filters). The native side reads the file (up to 15 MB) and hands it to the UI, which asks which trip it belongs to, upcoming trips first, then opens that trip's booking import with the file already chosen. --- .../android/app/src/main/AndroidManifest.xml | 10 +++ .../java/com/liketrek/trek/MainActivity.java | 7 ++ .../com/liketrek/trek/TrekSharedFiles.java | 76 ++++++++++++++++++ .../com/liketrek/trek/TrekShellPlugin.java | 18 ++++- client/ios/App/App/Info.plist | 21 +++++ client/ios/App/App/SceneDelegate.swift | 49 +++++++++++- client/ios/App/App/TrekShellPlugin.swift | 20 ++++- client/src/App.tsx | 2 + .../Layout/NativeImportSheet.test.tsx | 43 ++++++++++ .../components/Layout/NativeImportSheet.tsx | 80 +++++++++++++++++++ .../components/Planner/BookingImportModal.tsx | 7 ++ client/src/mobile/screens/trip/MTripShell.tsx | 9 +++ client/src/native/nativeApp.test.ts | 1 + client/src/native/nativeApp.ts | 3 + client/src/native/sharedFiles.test.ts | 53 ++++++++++++ client/src/native/sharedFiles.ts | 55 +++++++++++++ client/src/native/trekShell.ts | 14 ++++ shared/src/i18n/ar/native.ts | 2 + shared/src/i18n/az/native.ts | 2 + shared/src/i18n/br/native.ts | 2 + shared/src/i18n/ca/native.ts | 2 + shared/src/i18n/cs/native.ts | 2 + shared/src/i18n/de/native.ts | 2 + shared/src/i18n/en/native.ts | 2 + shared/src/i18n/es/native.ts | 2 + shared/src/i18n/et/native.ts | 2 + shared/src/i18n/fr/native.ts | 2 + shared/src/i18n/gr/native.ts | 2 + shared/src/i18n/he/native.ts | 2 + shared/src/i18n/hu/native.ts | 2 + shared/src/i18n/id/native.ts | 2 + shared/src/i18n/it/native.ts | 2 + shared/src/i18n/ja/native.ts | 2 + shared/src/i18n/ko/native.ts | 2 + shared/src/i18n/nl/native.ts | 2 + shared/src/i18n/pl/native.ts | 2 + shared/src/i18n/ru/native.ts | 2 + shared/src/i18n/sk/native.ts | 2 + shared/src/i18n/sv/native.ts | 2 + shared/src/i18n/th/native.ts | 2 + shared/src/i18n/tr/native.ts | 2 + shared/src/i18n/uk/native.ts | 2 + shared/src/i18n/vi/native.ts | 2 + shared/src/i18n/zh-TW/native.ts | 2 + shared/src/i18n/zh/native.ts | 2 + 45 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 client/android/app/src/main/java/com/liketrek/trek/TrekSharedFiles.java create mode 100644 client/src/components/Layout/NativeImportSheet.test.tsx create mode 100644 client/src/components/Layout/NativeImportSheet.tsx create mode 100644 client/src/native/sharedFiles.test.ts create mode 100644 client/src/native/sharedFiles.ts diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index ec0ca5e2b..e395c86a0 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -23,6 +23,16 @@ + + + + + + + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + Booking document + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + com.adobe.pdf + com.apple.ical.ics + com.apple.pkpass + com.apple.mail.email + public.email-message + + + + LSSupportsOpeningDocumentsInPlace + CFBundleURLTypes diff --git a/client/ios/App/App/SceneDelegate.swift b/client/ios/App/App/SceneDelegate.swift index 53978bc7d..f81a497d6 100644 --- a/client/ios/App/App/SceneDelegate.swift +++ b/client/ios/App/App/SceneDelegate.swift @@ -11,6 +11,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = TrekBridgeViewController() window?.makeKeyAndVisible() + // Launched by "Open in TREK" on a booking document. + for context in connectionOptions.urlContexts where context.url.isFileURL { + TrekSharedFiles.receive(context.url) + } SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions) } @@ -25,7 +29,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { - SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts) + // A booking document shared to TREK goes to the UI's import; anything + // else (the sign-in callback) stays with Capacitor. + let files = URLContexts.filter { $0.url.isFileURL } + for context in files { TrekSharedFiles.receive(context.url) } + let others = URLContexts.subtracting(files) + if !others.isEmpty { SceneDelegateProxy.shared.scene(scene, openURLContexts: others) } } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { @@ -58,3 +67,41 @@ enum TrekPrivacyCover { cover = nil } } + +/// Booking documents shared to TREK ("Open in TREK"), held until the UI takes +/// them (TrekShell.takeSharedFile) and announced if it is already running. +enum TrekSharedFiles { + /// Above this the file is not a booking confirmation, and base64 over the + /// bridge would only stall the app. + static let maxBytes = 15 * 1024 * 1024 + private(set) static var pending: [String: Any]? + static var onReceive: (([String: Any]) -> Void)? + + static func receive(_ url: URL) { + let scoped = url.startAccessingSecurityScopedResource() + defer { if scoped { url.stopAccessingSecurityScopedResource() } } + guard let data = try? Data(contentsOf: url), data.count <= maxBytes else { return } + let file: [String: Any] = [ + "name": url.lastPathComponent, + "mimeType": mimeType(for: url.pathExtension.lowercased()), + "data": data.base64EncodedString(), + ] + pending = file + onReceive?(file) + } + + static func take() -> [String: Any]? { + defer { pending = nil } + return pending + } + + private static func mimeType(for ext: String) -> String { + switch ext { + case "pdf": return "application/pdf" + case "ics": return "text/calendar" + case "pkpass": return "application/vnd.apple.pkpass" + case "eml": return "message/rfc822" + default: return "application/octet-stream" + } + } +} diff --git a/client/ios/App/App/TrekShellPlugin.swift b/client/ios/App/App/TrekShellPlugin.swift index d5f79bad2..e1c6f2a5f 100644 --- a/client/ios/App/App/TrekShellPlugin.swift +++ b/client/ios/App/App/TrekShellPlugin.swift @@ -26,6 +26,7 @@ public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPr CAPPluginMethod(name: "setPrivacyCover", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "startLocation", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "stopLocation", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "takeSharedFile", returnType: CAPPluginReturnPromise), ] /// What this binary offers the server's UI, raised with every native feature @@ -33,7 +34,24 @@ public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPr /// and a server can require a minimum (minNativeShellApi in /api/health/features). static let shellApi = 2 /// The native features by name, so the UI can turn each on by itself. - static let features: [String] = ["capabilities", "edgeBack", "tileCache", "biometricLock", "geolocation"] + static let features: [String] = ["capabilities", "edgeBack", "tileCache", "biometricLock", "geolocation", "sharedFiles"] + + // MARK: - Shared booking documents + + override public func load() { + TrekSharedFiles.onReceive = { [weak self] file in + DispatchQueue.main.async { self?.notifyListeners("sharedFile", data: file) } + } + } + + /// The document shared to TREK before the UI was there to hear of it. + @objc func takeSharedFile(_ call: CAPPluginCall) { + if let file = TrekSharedFiles.take() { + call.resolve(["file": file]) + } else { + call.resolve(["file": NSNull()]) + } + } // MARK: - Location diff --git a/client/src/App.tsx b/client/src/App.tsx index bc335d8b5..303af78fd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -34,6 +34,7 @@ import OfflineBanner from './components/Layout/OfflineBanner' import NewVersionNotice from './components/Layout/NewVersionNotice' import NativeUpdateNotice from './components/Layout/NativeUpdateNotice' import AppLockGate from './components/Layout/AppLockGate' +import NativeImportSheet from './components/Layout/NativeImportSheet' import { SystemNoticeHost } from './components/SystemNotices/SystemNoticeHost.js' import HelpPanel from './components/Help/HelpPanel' // Notice action registrations (side-effect imports): @@ -446,6 +447,7 @@ export default function App() { + {/* One boundary for all route chunks, above so it stays mounted across navigations. react-router runs location updates inside a transition, so a mounted boundary keeps the current page on screen instead of flashing diff --git a/client/src/components/Layout/NativeImportSheet.test.tsx b/client/src/components/Layout/NativeImportSheet.test.tsx new file mode 100644 index 000000000..74ed44ec7 --- /dev/null +++ b/client/src/components/Layout/NativeImportSheet.test.tsx @@ -0,0 +1,43 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { act, fireEvent, render, screen } from '../../../tests/helpers/render'; +import { tripsApi } from '../../api/client'; +import { useSharedImportStore } from '../../native/sharedFiles'; +import NativeImportSheet from './NativeImportSheet'; + +const navigate = vi.hoisted(() => vi.fn()); +vi.mock('react-router', async (importOriginal) => ({ + ...(await importOriginal()), + useNavigate: () => navigate, +})); + +beforeEach(() => { + navigate.mockReset(); + useSharedImportStore.setState({ file: null, tripId: null }); + vi.spyOn(tripsApi, 'list').mockResolvedValue({ + trips: [ + { id: 1, title: 'Past trip', start_date: '2020-01-01', end_date: '2020-01-05' }, + { id: 2, title: 'Japan', start_date: '2099-03-01', end_date: '2099-03-10' }, + ], + }); +}); + +describe('NativeImportSheet', () => { + it('asks which trip a shared document goes to, upcoming trips first, and opens it there', async () => { + render(); + act(() => useSharedImportStore.getState().setFile(new File(['x'], 'booking.pdf', { type: 'application/pdf' }))); + + const rows = await screen.findAllByRole('button', { name: /Japan|Past trip/ }); + expect(rows.map((r) => r.textContent)).toEqual(['Japan', 'Past trip']); + expect(screen.getByText('booking.pdf')).toBeInTheDocument(); + + fireEvent.click(rows[0]); + expect(useSharedImportStore.getState().tripId).toBe(2); + expect(navigate).toHaveBeenCalledWith('/trips/2'); + }); + + it('stays closed with nothing shared', () => { + render(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(tripsApi.list).not.toHaveBeenCalled(); + }); +}); diff --git a/client/src/components/Layout/NativeImportSheet.tsx b/client/src/components/Layout/NativeImportSheet.tsx new file mode 100644 index 000000000..c91a1e936 --- /dev/null +++ b/client/src/components/Layout/NativeImportSheet.tsx @@ -0,0 +1,80 @@ +/** + * NativeImportSheet: a booking document was shared to TREK (native/sharedFiles.ts), + * so this asks which trip it belongs to and opens that trip's booking import with + * the file already chosen. Trips still ahead come first, the ones that ended after. + */ +import { ChevronRight, FileText } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { tripsApi } from '../../api/client'; +import { useTranslation } from '../../i18n'; +import MSheet from '../../mobile/components/MSheet'; +import { useSharedImportStore } from '../../native/sharedFiles'; + +interface TripRow { + id: number; + title: string; + start_date?: string | null; + end_date?: string | null; +} + +export default function NativeImportSheet(): React.ReactElement | null { + const { t } = useTranslation(); + const navigate = useNavigate(); + const file = useSharedImportStore((s) => s.file); + const tripId = useSharedImportStore((s) => s.tripId); + const chooseTrip = useSharedImportStore((s) => s.chooseTrip); + const setFile = useSharedImportStore((s) => s.setFile); + const [trips, setTrips] = useState([]); + + const open = !!file && tripId == null; + + useEffect(() => { + if (!open) return; + tripsApi.list().then( + (data: { trips?: TripRow[] }) => { + const today = new Date().toISOString().slice(0, 10); + const list = [...(data.trips ?? [])]; + const ahead = (trip: TripRow) => (trip.end_date ?? '9999') >= today; + list.sort( + (a, b) => Number(ahead(b)) - Number(ahead(a)) || (a.start_date ?? '').localeCompare(b.start_date ?? '') + ); + setTrips(list); + }, + () => setTrips([]) + ); + }, [open]); + + const pick = (id: number) => { + chooseTrip(id); + navigate(`/trips/${id}`); + }; + + return ( + setFile(null)} variant="bottom" ariaLabel={t('native.share.title')}> +
+
{t('native.share.title')}
+ {file && ( +
+
+ )} +
+ {trips.map((trip) => ( + + ))} + {trips.length === 0 &&

{t('native.share.noTrips')}

} +
+
+
+ ); +} diff --git a/client/src/components/Planner/BookingImportModal.tsx b/client/src/components/Planner/BookingImportModal.tsx index aa84b9eef..8955e12ce 100644 --- a/client/src/components/Planner/BookingImportModal.tsx +++ b/client/src/components/Planner/BookingImportModal.tsx @@ -1,4 +1,5 @@ import { createPortal } from 'react-dom' +import { useSharedImportStore } from '../../native/sharedFiles' import { useState, useRef, useEffect } from 'react' import { Upload, X } from 'lucide-react' import { useTranslation } from '../../i18n' @@ -35,6 +36,12 @@ export default function BookingImportModal({ isOpen, onClose, tripId, kind }: Bo const mouseDownTarget = useRef(null) const [files, setFiles] = useState([]) + // A booking document shared to TREK lands here already chosen. + useEffect(() => { + if (!isOpen) return + const shared = useSharedImportStore.getState().file + if (shared) setFiles([useSharedImportStore.getState().take() ?? shared]) + }, [isOpen]) const [isDragOver, setIsDragOver] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState('') diff --git a/client/src/mobile/screens/trip/MTripShell.tsx b/client/src/mobile/screens/trip/MTripShell.tsx index cc69e8551..95526dd92 100644 --- a/client/src/mobile/screens/trip/MTripShell.tsx +++ b/client/src/mobile/screens/trip/MTripShell.tsx @@ -9,6 +9,7 @@ import { pickDockTabs } from './dockTabs' import MBadge from '../../components/MBadge' import MIconBtn from '../../components/MIconBtn' import { useActivePill } from '../../components/useActivePill' +import { useSharedImportStore } from '../../../native/sharedFiles' import MPlanTimeline from './plan/MPlanTimeline' import MMapArea from './map/MMapArea' import MPlacesBrowser from './places/MPlacesBrowser' @@ -291,6 +292,14 @@ export default function MTripShell({ }) }, [planner.selectedDayId, days, dayRail]) // The active day's pill glides to the next chip rather than jumping there. + // A booking document shared to TREK, and this trip picked for it: open the import. + const sharedTripId = useSharedImportStore(s => s.tripId) + useEffect(() => { + if (sharedTripId == null || sharedTripId !== Number(tripId)) return + planner.setBookingImportKind('bookings') + planner.setShowBookingImport(true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sharedTripId, tripId]) const dayPill = useActivePill(dayRail, '[aria-current="true"]', [planner.selectedDayId, days, dayRail, language]) const trTab = planner.activeTab diff --git a/client/src/native/nativeApp.test.ts b/client/src/native/nativeApp.test.ts index ba0cae970..f85b54f9d 100644 --- a/client/src/native/nativeApp.test.ts +++ b/client/src/native/nativeApp.test.ts @@ -23,6 +23,7 @@ vi.mock('./capabilities', () => ({ loadNativeCapabilities: vi.fn(async () => ({})), })); vi.mock('./geolocation', () => ({ installNativeGeolocation: vi.fn() })); +vi.mock('./sharedFiles', () => ({ startSharedFiles: vi.fn(async () => {}) })); vi.mock('../mobile/overlayStack', () => ({ closeTopOverlay: vi.fn(() => false) })); function backButtonHandler(): (event: { canGoBack: boolean }) => void { diff --git a/client/src/native/nativeApp.ts b/client/src/native/nativeApp.ts index efe7feff8..091d439bf 100644 --- a/client/src/native/nativeApp.ts +++ b/client/src/native/nativeApp.ts @@ -9,6 +9,7 @@ import { installNativeGeolocation } from './geolocation'; import { keepOfflineShellFresh } from './offlineShell'; import { isNativeApp, isNativeIos } from './platform'; import { reportNativeFailure } from './reportFailure'; +import { startSharedFiles } from './sharedFiles'; import { isUnimplemented } from './trekShell'; /** Long enough that the warm-up never competes with the first screen's requests. */ @@ -27,6 +28,8 @@ export function startNativeApp(): void { void checkAppUpdate(fetchServerFeatures); // The app's own location under navigator.geolocation, where the binary has it. void loadNativeCapabilities().then(() => installNativeGeolocation()); + // Booking documents shared to TREK, waiting for a trip to import into. + void startSharedFiles(); // A tap on the status bar scrolls the list under it back to the top, as in // every iOS app (the status-bar plugin fires statusTap on iOS). diff --git a/client/src/native/sharedFiles.test.ts b/client/src/native/sharedFiles.test.ts new file mode 100644 index 000000000..e7b41566d --- /dev/null +++ b/client/src/native/sharedFiles.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { hasNativeFeature } from './capabilities'; +import { startSharedFiles, toFile, useSharedImportStore } from './sharedFiles'; +import { TrekShell } from './trekShell'; + +vi.mock('./capabilities', () => ({ hasNativeFeature: vi.fn(), loadNativeCapabilities: vi.fn(async () => ({})) })); +vi.mock('./trekShell', () => ({ + TrekShell: { addListener: vi.fn(async () => ({ remove: async () => {} })), takeSharedFile: vi.fn() }, + isUnimplemented: () => false, +})); + +const PDF = { name: 'booking.pdf', mimeType: 'application/pdf', data: btoa('%PDF-1.4 hello') }; + +beforeEach(() => { + useSharedImportStore.setState({ file: null, tripId: null }); + vi.mocked(hasNativeFeature).mockReturnValue(true); + vi.mocked(TrekShell.takeSharedFile).mockReset().mockResolvedValue({ file: null }); +}); + +describe('shared booking documents', () => { + it('turns the base64 the native side sends into a File', async () => { + const file = toFile(PDF); + expect(file.name).toBe('booking.pdf'); + expect(file.type).toBe('application/pdf'); + expect(await file.text()).toBe('%PDF-1.4 hello'); + }); + + it('picks up a document shared before the UI ran, and the next ones as they come', async () => { + vi.mocked(TrekShell.takeSharedFile).mockResolvedValue({ file: PDF }); + await startSharedFiles(); + expect(useSharedImportStore.getState().file?.name).toBe('booking.pdf'); + + const listener = vi.mocked(TrekShell.addListener).mock.calls[0][1] as (p: typeof PDF) => void; + listener({ ...PDF, name: 'train.ics', mimeType: 'text/calendar' }); + expect(useSharedImportStore.getState().file?.name).toBe('train.ics'); + }); + + it('hands the file over once a trip is picked, and empties the queue', () => { + useSharedImportStore.getState().setFile(toFile(PDF)); + useSharedImportStore.getState().chooseTrip(7); + expect(useSharedImportStore.getState().tripId).toBe(7); + expect(useSharedImportStore.getState().take()?.name).toBe('booking.pdf'); + expect(useSharedImportStore.getState()).toMatchObject({ file: null, tripId: null }); + }); + + it('does nothing in a binary without the feature', async () => { + vi.mocked(hasNativeFeature).mockReturnValue(false); + vi.mocked(TrekShell.addListener).mockClear(); + await startSharedFiles(); + expect(TrekShell.addListener).not.toHaveBeenCalled(); + expect(TrekShell.takeSharedFile).not.toHaveBeenCalled(); + }); +}); diff --git a/client/src/native/sharedFiles.ts b/client/src/native/sharedFiles.ts new file mode 100644 index 000000000..830b9c00f --- /dev/null +++ b/client/src/native/sharedFiles.ts @@ -0,0 +1,55 @@ +import { create } from 'zustand'; +import { hasNativeFeature, loadNativeCapabilities } from './capabilities'; +import { reportNativeFailure } from './reportFailure'; +import { TrekShell, isUnimplemented, type SharedFilePayload } from './trekShell'; + +/** + * Booking documents shared to TREK ("Open in TREK" on a PDF, a calendar entry, + * a Wallet pass or a mail). The native side hands them over as base64 + * (TrekShell.takeSharedFile and the sharedFile event); here they become a File + * waiting for a trip: NativeImportSheet asks which one, and the trip's booking + * import opens with the file already chosen (BookingImportModal). + */ +interface SharedImportState { + file: File | null; + /** The trip the user picked for it; the planner of that trip takes it from here. */ + tripId: number | null; + setFile: (file: File | null) => void; + chooseTrip: (tripId: number) => void; + /** The booking import took the file: the queue is empty again. */ + take: () => File | null; +} + +export const useSharedImportStore = create((set, get) => ({ + file: null, + tripId: null, + setFile: (file) => set({ file, tripId: null }), + chooseTrip: (tripId) => set({ tripId }), + take: () => { + const { file } = get(); + set({ file: null, tripId: null }); + return file; + }, +})); + +export function toFile(payload: SharedFilePayload): File { + const binary = atob(payload.data); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return new File([bytes], payload.name || 'booking', { type: payload.mimeType || 'application/octet-stream' }); +} + +/** Picks up a document shared before the UI ran, and listens for the next ones. */ +export async function startSharedFiles(): Promise { + await loadNativeCapabilities(); + if (!hasNativeFeature('sharedFiles')) return; + const accept = (payload: SharedFilePayload | null) => { + if (payload?.data) useSharedImportStore.getState().setFile(toFile(payload)); + }; + void TrekShell.addListener('sharedFile', accept); + try { + accept((await TrekShell.takeSharedFile()).file); + } catch (err) { + if (!isUnimplemented(err)) reportNativeFailure(err); + } +} diff --git a/client/src/native/trekShell.ts b/client/src/native/trekShell.ts index afe9c0ad9..097323b60 100644 --- a/client/src/native/trekShell.ts +++ b/client/src/native/trekShell.ts @@ -49,6 +49,20 @@ export interface TrekShellPlugin { event: 'location', listener: (payload: NativeLocationPayload) => void ): Promise<{ remove: () => Promise }>; + addListener( + event: 'sharedFile', + listener: (payload: SharedFilePayload) => void + ): Promise<{ remove: () => Promise }>; + /** Feature sharedFiles: the booking document shared to TREK before the UI ran. */ + takeSharedFile(): Promise<{ file: SharedFilePayload | null }>; +} + +/** A document shared to TREK, as the native side reads it. */ +export interface SharedFilePayload { + name: string; + mimeType: string; + /** base64 */ + data: string; } /** A position, or an error shaped like GeolocationPositionError (1 denied, 2 unavailable). */ diff --git a/shared/src/i18n/ar/native.ts b/shared/src/i18n/ar/native.ts index f445c5288..0ded8f83f 100644 --- a/shared/src/i18n/ar/native.ts +++ b/shared/src/i18n/ar/native.ts @@ -80,6 +80,8 @@ const native: TranslationStrings = { 'native.lock.settingTitle': 'قفل التطبيق', 'native.lock.setting': 'القفل باستخدام Face ID أو رمز المرور', 'native.lock.settingHint': 'يطلب ذلك عند فتح TREK وبعد نصف دقيقة في الخلفية، ويخفي التطبيق في مبدّل التطبيقات.', + 'native.share.title': 'الاستيراد إلى أي رحلة؟', + 'native.share.noTrips': 'لا توجد رحلة بعد. أنشئ رحلة أولًا ثم شارك المستند مجددًا.', }; export default native; diff --git a/shared/src/i18n/az/native.ts b/shared/src/i18n/az/native.ts index aeba3db30..1c5b8b7f5 100644 --- a/shared/src/i18n/az/native.ts +++ b/shared/src/i18n/az/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Face ID və ya kodla kilidlə', 'native.lock.settingHint': 'TREK açılanda və arxa fonda yarım dəqiqədən sonra soruşur, tətbiqi tətbiq dəyişdiricisində gizlədir.', + 'native.share.title': 'Hansı səyahətə idxal edilsin?', + 'native.share.noTrips': 'Hələ səyahət yoxdur. Əvvəlcə birini yaradın, sonra sənədi yenidən paylaşın.', }; export default native; diff --git a/shared/src/i18n/br/native.ts b/shared/src/i18n/br/native.ts index 6005fb0b4..7a540640d 100644 --- a/shared/src/i18n/br/native.ts +++ b/shared/src/i18n/br/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Bloquear com Face ID ou código', 'native.lock.settingHint': 'Pede ao abrir o TREK e após meio minuto em segundo plano, e oculta o app no seletor de apps.', + 'native.share.title': 'Importar para qual viagem?', + 'native.share.noTrips': 'Ainda não há viagens. Crie uma primeiro e compartilhe o documento de novo.', }; export default native; diff --git a/shared/src/i18n/ca/native.ts b/shared/src/i18n/ca/native.ts index 6f7420077..c007e6386 100644 --- a/shared/src/i18n/ca/native.ts +++ b/shared/src/i18n/ca/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Bloqueja amb Face ID o codi', 'native.lock.settingHint': 'Ho demana en obrir TREK i després de mig minut en segon pla, i amaga l’app al selector d’apps.', + 'native.share.title': 'A quin viatge vols importar-ho?', + 'native.share.noTrips': 'Encara no hi ha cap viatge. Crea’n un i torna a compartir el document.', }; export default native; diff --git a/shared/src/i18n/cs/native.ts b/shared/src/i18n/cs/native.ts index b063a899f..73fba6fa8 100644 --- a/shared/src/i18n/cs/native.ts +++ b/shared/src/i18n/cs/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Zamknout pomocí Face ID nebo kódu', 'native.lock.settingHint': 'Zeptá se při otevření TREK a po půl minutě na pozadí a skryje aplikaci v přepínači aplikací.', + 'native.share.title': 'Importovat do které cesty?', + 'native.share.noTrips': 'Zatím žádná cesta. Nejdřív nějakou vytvořte a pak dokument sdílejte znovu.', }; export default native; diff --git a/shared/src/i18n/de/native.ts b/shared/src/i18n/de/native.ts index 4aac52592..39444941e 100644 --- a/shared/src/i18n/de/native.ts +++ b/shared/src/i18n/de/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Mit Face ID oder Code sperren', 'native.lock.settingHint': 'Fragt beim Öffnen und nach einer halben Minute im Hintergrund nach und verbirgt TREK im App-Umschalter.', + 'native.share.title': 'In welchen Trip importieren?', + 'native.share.noTrips': 'Noch kein Trip. Lege zuerst einen an und teile das Dokument dann erneut.', }; export default native; diff --git a/shared/src/i18n/en/native.ts b/shared/src/i18n/en/native.ts index 31f603099..35b668677 100644 --- a/shared/src/i18n/en/native.ts +++ b/shared/src/i18n/en/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Lock with Face ID or passcode', 'native.lock.settingHint': 'Asks when TREK opens and after half a minute in the background, and hides the app in the app switcher.', + 'native.share.title': 'Import into which trip?', + 'native.share.noTrips': 'No trip yet. Create one first, then share the document again.', }; export default native; diff --git a/shared/src/i18n/es/native.ts b/shared/src/i18n/es/native.ts index 4279a01f8..d54810e8a 100644 --- a/shared/src/i18n/es/native.ts +++ b/shared/src/i18n/es/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Bloquear con Face ID o código', 'native.lock.settingHint': 'Lo pide al abrir TREK y tras medio minuto en segundo plano, y oculta la app en el selector de apps.', + 'native.share.title': '¿En qué viaje importar?', + 'native.share.noTrips': 'Aún no hay viajes. Crea uno primero y vuelve a compartir el documento.', }; export default native; diff --git a/shared/src/i18n/et/native.ts b/shared/src/i18n/et/native.ts index 39facd3c5..98ecfb60b 100644 --- a/shared/src/i18n/et/native.ts +++ b/shared/src/i18n/et/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Lukusta Face ID või pääsukoodiga', 'native.lock.settingHint': 'Küsib TREK-i avamisel ja pärast poolt minutit taustal ning peidab rakenduse rakenduste vahetajas.', + 'native.share.title': 'Millisesse reisi importida?', + 'native.share.noTrips': 'Reise veel pole. Loo esmalt üks ja jaga dokumenti uuesti.', }; export default native; diff --git a/shared/src/i18n/fr/native.ts b/shared/src/i18n/fr/native.ts index 5329ee437..4a4fd48d7 100644 --- a/shared/src/i18n/fr/native.ts +++ b/shared/src/i18n/fr/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Verrouiller avec Face ID ou code', 'native.lock.settingHint': 'Le demande à l’ouverture de TREK et après une demi-minute en arrière-plan, et masque l’app dans le sélecteur d’apps.', + 'native.share.title': 'Importer dans quel voyage ?', + 'native.share.noTrips': 'Pas encore de voyage. Créez-en un, puis partagez à nouveau le document.', }; export default native; diff --git a/shared/src/i18n/gr/native.ts b/shared/src/i18n/gr/native.ts index 312b9dbbb..f666f08ee 100644 --- a/shared/src/i18n/gr/native.ts +++ b/shared/src/i18n/gr/native.ts @@ -85,6 +85,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Κλείδωμα με Face ID ή κωδικό', 'native.lock.settingHint': 'Ζητείται κατά το άνοιγμα του TREK και μετά από μισό λεπτό στο παρασκήνιο, και η εφαρμογή κρύβεται στην εναλλαγή εφαρμογών.', + 'native.share.title': 'Εισαγωγή σε ποιο ταξίδι;', + 'native.share.noTrips': 'Δεν υπάρχει ακόμα ταξίδι. Δημιουργήστε ένα πρώτα και κοινοποιήστε ξανά το έγγραφο.', }; export default native; diff --git a/shared/src/i18n/he/native.ts b/shared/src/i18n/he/native.ts index e201b220d..da74576c8 100644 --- a/shared/src/i18n/he/native.ts +++ b/shared/src/i18n/he/native.ts @@ -80,6 +80,8 @@ const native: TranslationStrings = { 'native.lock.settingTitle': 'נעילת אפליקציה', 'native.lock.setting': 'נעילה עם Face ID או קוד', 'native.lock.settingHint': 'מבקש בעת פתיחת TREK ואחרי חצי דקה ברקע, ומסתיר את האפליקציה במחליף האפליקציות.', + 'native.share.title': 'לייבא לאיזה טיול?', + 'native.share.noTrips': 'אין עדיין טיול. צרו טיול ואז שתפו את המסמך שוב.', }; export default native; diff --git a/shared/src/i18n/hu/native.ts b/shared/src/i18n/hu/native.ts index 29dec6f7e..194b29540 100644 --- a/shared/src/i18n/hu/native.ts +++ b/shared/src/i18n/hu/native.ts @@ -84,6 +84,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Zárolás Face ID-val vagy kóddal', 'native.lock.settingHint': 'Rákérdez a TREK megnyitásakor és fél perc háttérben töltött idő után, és elrejti az alkalmazást az alkalmazásváltóban.', + 'native.share.title': 'Melyik utazásba importáljuk?', + 'native.share.noTrips': 'Még nincs utazás. Előbb hozz létre egyet, aztán oszd meg újra a dokumentumot.', }; export default native; diff --git a/shared/src/i18n/id/native.ts b/shared/src/i18n/id/native.ts index 8df8c449d..47e658e33 100644 --- a/shared/src/i18n/id/native.ts +++ b/shared/src/i18n/id/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Kunci dengan Face ID atau kode sandi', 'native.lock.settingHint': 'Meminta saat TREK dibuka dan setelah setengah menit di latar belakang, serta menyembunyikan aplikasi di pengalih aplikasi.', + 'native.share.title': 'Impor ke perjalanan mana?', + 'native.share.noTrips': 'Belum ada perjalanan. Buat satu dulu, lalu bagikan dokumennya lagi.', }; export default native; diff --git a/shared/src/i18n/it/native.ts b/shared/src/i18n/it/native.ts index f245287a4..7891b8ec8 100644 --- a/shared/src/i18n/it/native.ts +++ b/shared/src/i18n/it/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Blocca con Face ID o codice', 'native.lock.settingHint': 'Lo chiede all’apertura di TREK e dopo mezzo minuto in background, e nasconde l’app nel selettore delle app.', + 'native.share.title': 'Importare in quale viaggio?', + 'native.share.noTrips': 'Ancora nessun viaggio. Creane uno, poi condividi di nuovo il documento.', }; export default native; diff --git a/shared/src/i18n/ja/native.ts b/shared/src/i18n/ja/native.ts index 47c78932e..1c87a190d 100644 --- a/shared/src/i18n/ja/native.ts +++ b/shared/src/i18n/ja/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Face IDまたはパスコードでロック', 'native.lock.settingHint': 'TREKを開いたときと、バックグラウンドで30秒経過したあとに確認し、アプリスイッチャーでは内容を隠します。', + 'native.share.title': 'どの旅行に取り込みますか?', + 'native.share.noTrips': 'まだ旅行がありません。先に作成してから、もう一度ドキュメントを共有してください。', }; export default native; diff --git a/shared/src/i18n/ko/native.ts b/shared/src/i18n/ko/native.ts index 806778b9c..522ba27aa 100644 --- a/shared/src/i18n/ko/native.ts +++ b/shared/src/i18n/ko/native.ts @@ -81,6 +81,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Face ID 또는 암호로 잠금', 'native.lock.settingHint': 'TREK을 열 때와 백그라운드에서 30초가 지난 후에 확인하며, 앱 전환기에서는 내용을 숨깁니다.', + 'native.share.title': '어느 여행으로 가져올까요?', + 'native.share.noTrips': '아직 여행이 없습니다. 먼저 여행을 만든 뒤 문서를 다시 공유하세요.', }; export default native; diff --git a/shared/src/i18n/nl/native.ts b/shared/src/i18n/nl/native.ts index 77194910e..430a3bbef 100644 --- a/shared/src/i18n/nl/native.ts +++ b/shared/src/i18n/nl/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Vergrendelen met Face ID of toegangscode', 'native.lock.settingHint': 'Vraagt erom bij het openen van TREK en na een halve minuut op de achtergrond, en verbergt de app in de appkiezer.', + 'native.share.title': 'In welke reis importeren?', + 'native.share.noTrips': 'Nog geen reis. Maak er eerst een aan en deel het document dan opnieuw.', }; export default native; diff --git a/shared/src/i18n/pl/native.ts b/shared/src/i18n/pl/native.ts index 31c9153de..8b17f18f1 100644 --- a/shared/src/i18n/pl/native.ts +++ b/shared/src/i18n/pl/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Blokuj za pomocą Face ID lub kodu', 'native.lock.settingHint': 'Pyta przy otwieraniu TREK i po pół minuty w tle oraz ukrywa aplikację w przełączniku aplikacji.', + 'native.share.title': 'Do której podróży zaimportować?', + 'native.share.noTrips': 'Nie ma jeszcze podróży. Najpierw utwórz podróż, a potem udostępnij dokument ponownie.', }; export default native; diff --git a/shared/src/i18n/ru/native.ts b/shared/src/i18n/ru/native.ts index f08233c3e..976e0a0dd 100644 --- a/shared/src/i18n/ru/native.ts +++ b/shared/src/i18n/ru/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Блокировать с Face ID или кодом', 'native.lock.settingHint': 'Запрашивается при открытии TREK и через полминуты в фоне, а в переключателе приложений содержимое скрыто.', + 'native.share.title': 'В какую поездку импортировать?', + 'native.share.noTrips': 'Поездок пока нет. Сначала создайте поездку, затем снова поделитесь документом.', }; export default native; diff --git a/shared/src/i18n/sk/native.ts b/shared/src/i18n/sk/native.ts index 85a19799d..a140f0352 100644 --- a/shared/src/i18n/sk/native.ts +++ b/shared/src/i18n/sk/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Zamknúť pomocou Face ID alebo kódu', 'native.lock.settingHint': 'Opýta sa pri otvorení TREK a po pol minúte na pozadí a skryje aplikáciu v prepínači aplikácií.', + 'native.share.title': 'Importovať do ktorej cesty?', + 'native.share.noTrips': 'Zatiaľ žiadna cesta. Najprv nejakú vytvorte a potom dokument zdieľajte znova.', }; export default native; diff --git a/shared/src/i18n/sv/native.ts b/shared/src/i18n/sv/native.ts index 51c460c96..fe3787c79 100644 --- a/shared/src/i18n/sv/native.ts +++ b/shared/src/i18n/sv/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Lås med Face ID eller lösenkod', 'native.lock.settingHint': 'Frågar när TREK öppnas och efter en halv minut i bakgrunden, och döljer appen i appväxlaren.', + 'native.share.title': 'Importera till vilken resa?', + 'native.share.noTrips': 'Ingen resa ännu. Skapa en först och dela sedan dokumentet igen.', }; export default native; diff --git a/shared/src/i18n/th/native.ts b/shared/src/i18n/th/native.ts index 176131319..5d11811cb 100644 --- a/shared/src/i18n/th/native.ts +++ b/shared/src/i18n/th/native.ts @@ -80,6 +80,8 @@ const native: TranslationStrings = { 'native.lock.settingTitle': 'ล็อกแอป', 'native.lock.setting': 'ล็อกด้วย Face ID หรือรหัส', 'native.lock.settingHint': 'ถามเมื่อเปิด TREK และหลังจากอยู่เบื้องหลังครึ่งนาที และซ่อนแอปในตัวสลับแอป', + 'native.share.title': 'นำเข้าไปยังทริปไหน', + 'native.share.noTrips': 'ยังไม่มีทริป สร้างทริปก่อนแล้วแชร์เอกสารอีกครั้ง', }; export default native; diff --git a/shared/src/i18n/tr/native.ts b/shared/src/i18n/tr/native.ts index 06e87ba05..deebeaf4e 100644 --- a/shared/src/i18n/tr/native.ts +++ b/shared/src/i18n/tr/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Face ID veya parola ile kilitle', 'native.lock.settingHint': 'TREK açılırken ve arka planda yarım dakika sonra sorar, uygulamayı uygulama değiştiricide gizler.', + 'native.share.title': 'Hangi geziye aktarılsın?', + 'native.share.noTrips': 'Henüz gezi yok. Önce bir gezi oluşturun, sonra belgeyi yeniden paylaşın.', }; export default native; diff --git a/shared/src/i18n/uk/native.ts b/shared/src/i18n/uk/native.ts index 64dc63398..d2bb8f289 100644 --- a/shared/src/i18n/uk/native.ts +++ b/shared/src/i18n/uk/native.ts @@ -82,6 +82,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Блокувати за допомогою Face ID або коду', 'native.lock.settingHint': 'Запитує під час відкриття TREK і після пів хвилини у фоні, а в перемикачі застосунків вміст приховано.', + 'native.share.title': 'До якої подорожі імпортувати?', + 'native.share.noTrips': 'Подорожей ще немає. Спершу створіть подорож, а потім знову поділіться документом.', }; export default native; diff --git a/shared/src/i18n/vi/native.ts b/shared/src/i18n/vi/native.ts index 5902d6590..c8134c429 100644 --- a/shared/src/i18n/vi/native.ts +++ b/shared/src/i18n/vi/native.ts @@ -83,6 +83,8 @@ const native: TranslationStrings = { 'native.lock.setting': 'Khóa bằng Face ID hoặc mật mã', 'native.lock.settingHint': 'Hỏi khi mở TREK và sau nửa phút chạy nền, đồng thời ẩn ứng dụng trong trình chuyển ứng dụng.', + 'native.share.title': 'Nhập vào chuyến đi nào?', + 'native.share.noTrips': 'Chưa có chuyến đi nào. Hãy tạo một chuyến trước rồi chia sẻ lại tài liệu.', }; export default native; diff --git a/shared/src/i18n/zh-TW/native.ts b/shared/src/i18n/zh-TW/native.ts index 71ba73f2c..e781ff8f3 100644 --- a/shared/src/i18n/zh-TW/native.ts +++ b/shared/src/i18n/zh-TW/native.ts @@ -80,6 +80,8 @@ const native: TranslationStrings = { 'native.lock.settingTitle': 'App 鎖定', 'native.lock.setting': '使用 Face ID 或密碼鎖定', 'native.lock.settingHint': '開啟 TREK 時以及在背景停留半分鐘後會要求驗證,並在 App 切換器中隱藏內容。', + 'native.share.title': '要匯入到哪個行程?', + 'native.share.noTrips': '還沒有行程。請先建立一個,然後再次分享該文件。', }; export default native; diff --git a/shared/src/i18n/zh/native.ts b/shared/src/i18n/zh/native.ts index b7f9e74ec..3e6a8cc2d 100644 --- a/shared/src/i18n/zh/native.ts +++ b/shared/src/i18n/zh/native.ts @@ -80,6 +80,8 @@ const native: TranslationStrings = { 'native.lock.settingTitle': '应用锁', 'native.lock.setting': '使用面容 ID 或密码锁定', 'native.lock.settingHint': '打开 TREK 时以及在后台停留半分钟后会要求验证,并在应用切换器中隐藏内容。', + 'native.share.title': '导入到哪个行程?', + 'native.share.noTrips': '还没有行程。请先创建一个,然后再次分享该文档。', }; export default native;