feat(reservations): native booking-confirmation import via KDE KItinerary (#1102)

* feat(reservations): native booking-confirmation import via KDE KItinerary

Adds a two-step preview → confirm flow for importing booking emails,
PDFs, PKPass and HTML confirmations. The server invokes the KDE
kitinerary-extractor binary, maps JSON-LD schema.org output to TREK
reservation shapes, and persists via the existing createReservation
pipeline (accommodations, budget, places, WebSocket broadcasts).

- NestJS BookingImportModule: preview + confirm endpoints under
  /api/trips/:tripId/reservations/import/booking{,/confirm}
- KitineraryExtractorService: spawns the binary, filters stderr noise,
  handles QDateTime (@value) timezone-aware datetimes
- kitinerary-mapper: FlightReservation, TrainReservation, BusReservation,
  BoatReservation, LodgingReservation, FoodEstablishmentReservation,
  RentalCarReservation, EventReservation → typed preview items
- BookingImportService: auto-creates place rows; geocodes venues without
  coordinates via Nominatim (name+address → address → name fallback);
  resolves day IDs for accommodation linking
- BookingImportModal: drag-and-drop multi-file upload, preview cards
  with type icons, per-item exclude toggle, confirm step
- Shared Zod contracts: BookingImportPreviewItem, PreviewResponse,
  ConfirmRequest, ConfirmResponse — consumed by controller, service,
  API client and modal
- Dockerfile: node:24-trixie-slim runtime; amd64 downloads KDE static
  binary + locales; arm64 installs libkitinerary-bin + symlinks to
  fixed path; ENV KITINERARY_EXTRACTOR_PATH set for both arches
- /api/health/features exposes { bookingImport: boolean } so the UI
  hides the Import button when the binary is absent
- i18n keys (English), wiki docs, API.md, README one-liner

* i18n: add booking import translations for all 19 non-English locales

Adds 17 reservations.import.* keys and undo.importBooking to ar, br, cs,
de, es, fr, gr, hu, id, it, ja, ko, nl, pl, ru, tr, uk, zh, zh-TW.

* chore: enforce i18n parity

* docs(wiki): add KItinerary local setup instructions to dev environment guide
This commit is contained in:
jubnl
2026-06-04 20:40:57 +02:00
committed by GitHub
parent abe1c549bd
commit 6ef3c7ae6b
64 changed files with 1851 additions and 31 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
"format:check": "prettier --check \"src/**/*.ts\"",
"lint": "eslint --fix \"src/**/*.ts\"",
"i18n:parity": "node scripts/i18n-parity.mjs",
"i18n:parity:strict": "node scripts/i18n-parity.mjs --strict --files-only"
"i18n:parity:strict": "node scripts/i18n-parity.mjs --strict"
},
"dependencies": {
"isomorphic-dompurify": "^3.15.0",
+17
View File
@@ -118,5 +118,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
'reservations.addBooking': 'إضافة حجز',
'reservations.import.title': 'استيراد تأكيدات الحجز',
'reservations.import.cta': 'استيراد من ملف',
'reservations.import.dropHere': 'أسقط ملفات تأكيد الحجز هنا أو انقر للتحديد',
'reservations.import.dropActive': 'أسقط الملفات للاستيراد',
'reservations.import.acceptedFormats': 'المقبول: EML، PDF، PKPass، HTML، TXT (بحد أقصى 10 ميغابايت لكل ملف، حتى 5 ملفات)',
'reservations.import.parsing': 'جارٍ معالجة الملفات…',
'reservations.import.previewHeading': 'تم العثور على {count} حجز/حجوزات',
'reservations.import.previewEmpty': 'تعذّر استخراج أي حجوزات من الملفات المُحمَّلة.',
'reservations.import.removeItem': 'إزالة',
'reservations.import.confirm': 'استيراد {count} حجز/حجوزات',
'reservations.import.back': 'رجوع',
'reservations.import.success': 'تم استيراد {count} حجز/حجوزات',
'reservations.import.partialFailure': 'تم استيراد {created}، فشل {failed}',
'reservations.import.error': 'فشلت المعالجة. تأكد من أن الملف تأكيد حجز صالح.',
'reservations.import.unavailable': 'استيراد الحجوزات غير متاح على هذا الخادم.',
'reservations.import.unsupportedFormat': 'صيغة ملف غير مدعومة. استخدم EML أو PDF أو PKPass أو HTML أو TXT.',
'reservations.import.fileTooLarge': 'الملف "{name}" يتجاوز حد 10 ميغابايت.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'استيراد خرائط Naver',
'undo.addPlace': 'تمت إضافة المكان',
'undo.done': 'تم التراجع: {action}',
'undo.importBooking': 'استيراد تأكيد الحجز',
};
export default undo;
+17
View File
@@ -119,5 +119,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'A data/hora final deve ser posterior à data/hora inicial',
'reservations.addBooking': 'Adicionar reserva',
'reservations.import.title': 'Importar confirmações de reserva',
'reservations.import.cta': 'Importar de arquivo',
'reservations.import.dropHere': 'Solte os arquivos de confirmação de reserva aqui ou clique para selecionar',
'reservations.import.dropActive': 'Solte os arquivos para importar',
'reservations.import.acceptedFormats': 'Aceitos: EML, PDF, PKPass, HTML, TXT (máx. 10 MB cada, até 5 arquivos)',
'reservations.import.parsing': 'Analisando arquivos…',
'reservations.import.previewHeading': '{count} reserva(s) encontrada(s)',
'reservations.import.previewEmpty': 'Nenhuma reserva pôde ser extraída dos arquivos enviados.',
'reservations.import.removeItem': 'Remover',
'reservations.import.confirm': 'Importar {count} reserva(s)',
'reservations.import.back': 'Voltar',
'reservations.import.success': '{count} reserva(s) importada(s)',
'reservations.import.partialFailure': '{created} importada(s), {failed} falhou/falharam',
'reservations.import.error': 'Falha na análise. Verifique se o arquivo é uma confirmação de reserva válida.',
'reservations.import.unavailable': 'A importação de reservas não está disponível neste servidor.',
'reservations.import.unsupportedFormat': 'Formato de arquivo não suportado. Use EML, PDF, PKPass, HTML ou TXT.',
'reservations.import.fileTooLarge': 'O arquivo "{name}" excede o limite de 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Importação do Naver Maps',
'undo.addPlace': 'Local adicionado',
'undo.done': 'Desfeito: {action}',
'undo.importBooking': 'Importação de confirmação de reserva',
};
export default undo;
+17
View File
@@ -118,5 +118,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Datum/čas konce musí být po datu/čase začátku',
'reservations.addBooking': 'Přidat rezervaci',
'reservations.import.title': 'Importovat potvrzení rezervace',
'reservations.import.cta': 'Importovat ze souboru',
'reservations.import.dropHere': 'Přetáhněte soubory s potvrzením rezervace sem nebo klikněte pro výběr',
'reservations.import.dropActive': 'Pusťte soubory pro import',
'reservations.import.acceptedFormats': 'Přijímané formáty: EML, PDF, PKPass, HTML, TXT (max. 10 MB každý, až 5 souborů)',
'reservations.import.parsing': 'Zpracování souborů…',
'reservations.import.previewHeading': 'Nalezeno {count} rezervace/í',
'reservations.import.previewEmpty': 'Z nahraných souborů se nepodařilo extrahovat žádné rezervace.',
'reservations.import.removeItem': 'Odebrat',
'reservations.import.confirm': 'Importovat {count} rezervaci/í',
'reservations.import.back': 'Zpět',
'reservations.import.success': '{count} rezervace/í importováno',
'reservations.import.partialFailure': '{created} importováno, {failed} selhalo',
'reservations.import.error': 'Zpracování selhalo. Ujistěte se, že soubor je platným potvrzením rezervace.',
'reservations.import.unavailable': 'Import rezervací není na tomto serveru k dispozici.',
'reservations.import.unsupportedFormat': 'Nepodporovaný formát souboru. Použijte EML, PDF, PKPass, HTML nebo TXT.',
'reservations.import.fileTooLarge': 'Soubor „{name}" překračuje limit 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Import z Naver Maps',
'undo.addPlace': 'Místo přidáno',
'undo.done': 'Vráceno zpět: {action}',
'undo.importBooking': 'Import potvrzení rezervace',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Enddatum/-zeit muss nach dem Startdatum/-zeit liegen',
'reservations.addBooking': 'Buchung hinzufügen',
'reservations.import.title': 'Buchungsbestätigungen importieren',
'reservations.import.cta': 'Aus Datei importieren',
'reservations.import.dropHere': 'Buchungsbestätigungsdateien hier ablegen oder klicken zum Auswählen',
'reservations.import.dropActive': 'Dateien zum Importieren ablegen',
'reservations.import.acceptedFormats': 'Akzeptiert: EML, PDF, PKPass, HTML, TXT (max. 10 MB pro Datei, bis zu 5 Dateien)',
'reservations.import.parsing': 'Dateien werden verarbeitet…',
'reservations.import.previewHeading': '{count} Reservierung(en) gefunden',
'reservations.import.previewEmpty': 'Aus den hochgeladenen Dateien konnten keine Reservierungen extrahiert werden.',
'reservations.import.removeItem': 'Entfernen',
'reservations.import.confirm': '{count} Reservierung(en) importieren',
'reservations.import.back': 'Zurück',
'reservations.import.success': '{count} Reservierung(en) importiert',
'reservations.import.partialFailure': '{created} importiert, {failed} fehlgeschlagen',
'reservations.import.error': 'Verarbeitung fehlgeschlagen. Stellen Sie sicher, dass die Datei eine gültige Buchungsbestätigung ist.',
'reservations.import.unavailable': 'Buchungsimport ist auf diesem Server nicht verfügbar.',
'reservations.import.unsupportedFormat': 'Nicht unterstütztes Dateiformat. Verwenden Sie EML, PDF, PKPass, HTML oder TXT.',
'reservations.import.fileTooLarge': 'Datei „{name}" überschreitet das 10-MB-Limit.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naver Maps-Import',
'undo.addPlace': 'Ort hinzugefügt',
'undo.done': 'Rückgängig gemacht: {action}',
'undo.importBooking': 'Buchungsbestätigung-Import',
};
export default undo;
+17
View File
@@ -119,5 +119,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'End date/time must be after start date/time',
'reservations.addBooking': 'Add booking',
'reservations.import.title': 'Import booking confirmations',
'reservations.import.cta': 'Import from file',
'reservations.import.dropHere': 'Drop booking confirmation files here, or click to select',
'reservations.import.dropActive': 'Drop files to import',
'reservations.import.acceptedFormats': 'Accepted: EML, PDF, PKPass, HTML, TXT (max 10 MB each, up to 5 files)',
'reservations.import.parsing': 'Parsing files…',
'reservations.import.previewHeading': '{count} reservation(s) found',
'reservations.import.previewEmpty': 'No reservations could be extracted from the uploaded files.',
'reservations.import.removeItem': 'Remove',
'reservations.import.confirm': 'Import {count} reservation(s)',
'reservations.import.back': 'Back',
'reservations.import.success': '{count} reservation(s) imported',
'reservations.import.partialFailure': '{created} imported, {failed} failed',
'reservations.import.error': 'Parsing failed. Make sure the file is a valid booking confirmation.',
'reservations.import.unavailable': 'Booking import is not available on this server.',
'reservations.import.unsupportedFormat': 'Unsupported file format. Use EML, PDF, PKPass, HTML, or TXT.',
'reservations.import.fileTooLarge': 'File "{name}" exceeds 10 MB limit.',
};
export default reservations;
+1
View File
@@ -15,6 +15,7 @@ const undo: TranslationStrings = {
'undo.importKeyholeMarkup': 'KMZ/KML import',
'undo.importGoogleList': 'Google Maps import',
'undo.importNaverList': 'Naver Maps import',
'undo.importBooking': 'Booking confirmation import',
'undo.addPlace': 'Place added',
'undo.done': 'Undone: {action}',
};
+17
View File
@@ -119,5 +119,22 @@ const reservations: TranslationStrings = {
'reservations.meta.fromDay': 'Desde',
'reservations.meta.toDay': 'Hasta',
'reservations.meta.selectDay': 'Seleccionar día',
'reservations.import.title': 'Importar confirmaciones de reserva',
'reservations.import.cta': 'Importar desde archivo',
'reservations.import.dropHere': 'Suelta los archivos de confirmación de reserva aquí o haz clic para seleccionar',
'reservations.import.dropActive': 'Suelta los archivos para importar',
'reservations.import.acceptedFormats': 'Aceptados: EML, PDF, PKPass, HTML, TXT (máx. 10 MB por archivo, hasta 5 archivos)',
'reservations.import.parsing': 'Analizando archivos…',
'reservations.import.previewHeading': '{count} reserva(s) encontrada(s)',
'reservations.import.previewEmpty': 'No se pudieron extraer reservas de los archivos subidos.',
'reservations.import.removeItem': 'Eliminar',
'reservations.import.confirm': 'Importar {count} reserva(s)',
'reservations.import.back': 'Atrás',
'reservations.import.success': '{count} reserva(s) importada(s)',
'reservations.import.partialFailure': '{created} importada(s), {failed} fallida(s)',
'reservations.import.error': 'Error al analizar. Asegúrate de que el archivo sea una confirmación de reserva válida.',
'reservations.import.unavailable': 'La importación de reservas no está disponible en este servidor.',
'reservations.import.unsupportedFormat': 'Formato de archivo no compatible. Usa EML, PDF, PKPass, HTML o TXT.',
'reservations.import.fileTooLarge': 'El archivo «{name}» supera el límite de 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Importación de Naver Maps',
'undo.addPlace': 'Lugar agregado',
'undo.done': 'Deshecho: {action}',
'undo.importBooking': 'Importar confirmación de reserva',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'La date/heure de fin doit être postérieure à la date/heure de début',
'reservations.addBooking': 'Ajouter une réservation',
'reservations.import.title': 'Importer des confirmations de réservation',
'reservations.import.cta': 'Importer depuis un fichier',
'reservations.import.dropHere': 'Déposez les fichiers de confirmation de réservation ici ou cliquez pour sélectionner',
'reservations.import.dropActive': 'Déposez les fichiers pour importer',
'reservations.import.acceptedFormats': "Acceptés : EML, PDF, PKPass, HTML, TXT (max. 10 Mo chacun, jusqu'à 5 fichiers)",
'reservations.import.parsing': 'Analyse des fichiers…',
'reservations.import.previewHeading': '{count} réservation(s) trouvée(s)',
'reservations.import.previewEmpty': "Aucune réservation n'a pu être extraite des fichiers envoyés.",
'reservations.import.removeItem': 'Supprimer',
'reservations.import.confirm': 'Importer {count} réservation(s)',
'reservations.import.back': 'Retour',
'reservations.import.success': '{count} réservation(s) importée(s)',
'reservations.import.partialFailure': '{created} importée(s), {failed} échouée(s)',
'reservations.import.error': 'Analyse échouée. Assurez-vous que le fichier est une confirmation de réservation valide.',
'reservations.import.unavailable': "L'import de réservations n'est pas disponible sur ce serveur.",
'reservations.import.unsupportedFormat': 'Format de fichier non pris en charge. Utilisez EML, PDF, PKPass, HTML ou TXT.',
'reservations.import.fileTooLarge': 'Le fichier « {name} » dépasse la limite de 10 Mo.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Import Naver Maps',
'undo.addPlace': 'Lieu ajouté',
'undo.done': 'Annulé : {action}',
'undo.importBooking': 'Import de confirmation de réservation',
};
export default undo;
+17
View File
@@ -121,5 +121,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Η ημερομηνία/ώρα λήξης πρέπει να είναι μετά την ημερομηνία/ώρα έναρξης',
'reservations.addBooking': 'Προσθήκη κράτησης',
'reservations.import.title': 'Εισαγωγή επιβεβαιώσεων κράτησης',
'reservations.import.cta': 'Εισαγωγή από αρχείο',
'reservations.import.dropHere': 'Αποθέστε αρχεία επιβεβαίωσης κράτησης εδώ ή κάντε κλικ για επιλογή',
'reservations.import.dropActive': 'Αποθέστε αρχεία για εισαγωγή',
'reservations.import.acceptedFormats': 'Αποδεκτά: EML, PDF, PKPass, HTML, TXT (μέγιστο 10 MB το καθένα, έως 5 αρχεία)',
'reservations.import.parsing': 'Επεξεργασία αρχείων…',
'reservations.import.previewHeading': 'Βρέθηκαν {count} κράτηση/κρατήσεις',
'reservations.import.previewEmpty': 'Δεν ήταν δυνατή η εξαγωγή κρατήσεων από τα μεταφορτωμένα αρχεία.',
'reservations.import.removeItem': 'Αφαίρεση',
'reservations.import.confirm': 'Εισαγωγή {count} κράτησης/κρατήσεων',
'reservations.import.back': 'Πίσω',
'reservations.import.success': '{count} κράτηση/κρατήσεις εισήχθησαν',
'reservations.import.partialFailure': '{created} εισήχθησαν, {failed} απέτυχαν',
'reservations.import.error': 'Η επεξεργασία απέτυχε. Βεβαιωθείτε ότι το αρχείο είναι έγκυρη επιβεβαίωση κράτησης.',
'reservations.import.unavailable': 'Η εισαγωγή κρατήσεων δεν είναι διαθέσιμη σε αυτόν τον διακομιστή.',
'reservations.import.unsupportedFormat': 'Μη υποστηριζόμενη μορφή αρχείου. Χρησιμοποιήστε EML, PDF, PKPass, HTML ή TXT.',
'reservations.import.fileTooLarge': 'Το αρχείο «{name}» υπερβαίνει το όριο των 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Εισαγωγή Naver Maps',
'undo.addPlace': 'Η τοποθεσία προστέθηκε',
'undo.done': 'Αναιρέθηκε: {action}',
'undo.importBooking': 'Εισαγωγή επιβεβαίωσης κράτησης',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'A befejezés dátuma/időpontja a kezdés utáni kell legyen',
'reservations.addBooking': 'Foglalás hozzáadása',
'reservations.import.title': 'Foglalási visszaigazolások importálása',
'reservations.import.cta': 'Importálás fájlból',
'reservations.import.dropHere': 'Dobja ide a foglalási visszaigazolás fájlokat, vagy kattintson a kiválasztáshoz',
'reservations.import.dropActive': 'Dobja ide a fájlokat az importáláshoz',
'reservations.import.acceptedFormats': 'Elfogadott: EML, PDF, PKPass, HTML, TXT (max. 10 MB darabonként, legfeljebb 5 fájl)',
'reservations.import.parsing': 'Fájlok feldolgozása…',
'reservations.import.previewHeading': '{count} foglalás találva',
'reservations.import.previewEmpty': 'A feltöltött fájlokból nem sikerült foglalásokat kinyerni.',
'reservations.import.removeItem': 'Eltávolítás',
'reservations.import.confirm': '{count} foglalás importálása',
'reservations.import.back': 'Vissza',
'reservations.import.success': '{count} foglalás importálva',
'reservations.import.partialFailure': '{created} importálva, {failed} sikertelen',
'reservations.import.error': 'A feldolgozás sikertelen. Győződjön meg arról, hogy a fájl érvényes foglalási visszaigazolás.',
'reservations.import.unavailable': 'A foglalásimportálás nem érhető el ezen a kiszolgálón.',
'reservations.import.unsupportedFormat': 'Nem támogatott fájlformátum. Használjon EML, PDF, PKPass, HTML vagy TXT formátumot.',
'reservations.import.fileTooLarge': 'A(z) „{name}" fájl meghaladja a 10 MB-os korlátot.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naver Maps importálás',
'undo.addPlace': 'Hely hozzáadva',
'undo.done': 'Visszavonva: {action}',
'undo.importBooking': 'Foglalási visszaigazolás importálása',
};
export default undo;
+17
View File
@@ -119,5 +119,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Tanggal/waktu selesai harus setelah tanggal/waktu mulai',
'reservations.addBooking': 'Tambah pemesanan',
'reservations.import.title': 'Impor konfirmasi pemesanan',
'reservations.import.cta': 'Impor dari file',
'reservations.import.dropHere': 'Seret file konfirmasi pemesanan ke sini atau klik untuk memilih',
'reservations.import.dropActive': 'Lepaskan file untuk mengimpor',
'reservations.import.acceptedFormats': 'Diterima: EML, PDF, PKPass, HTML, TXT (maks. 10 MB per file, hingga 5 file)',
'reservations.import.parsing': 'Memproses file…',
'reservations.import.previewHeading': '{count} pemesanan ditemukan',
'reservations.import.previewEmpty': 'Tidak ada pemesanan yang dapat diekstrak dari file yang diunggah.',
'reservations.import.removeItem': 'Hapus',
'reservations.import.confirm': 'Impor {count} pemesanan',
'reservations.import.back': 'Kembali',
'reservations.import.success': '{count} pemesanan berhasil diimpor',
'reservations.import.partialFailure': '{created} berhasil diimpor, {failed} gagal',
'reservations.import.error': 'Pemrosesan gagal. Pastikan file adalah konfirmasi pemesanan yang valid.',
'reservations.import.unavailable': 'Impor pemesanan tidak tersedia di server ini.',
'reservations.import.unsupportedFormat': 'Format file tidak didukung. Gunakan EML, PDF, PKPass, HTML, atau TXT.',
'reservations.import.fileTooLarge': 'File "{name}" melebihi batas 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Impor Naver Maps',
'undo.addPlace': 'Tempat ditambahkan',
'undo.done': 'Dibatalkan: {action}',
'undo.importBooking': 'Impor konfirmasi pemesanan',
};
export default undo;
+17
View File
@@ -121,5 +121,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'La data/ora di fine deve essere successiva alla data/ora di inizio',
'reservations.addBooking': 'Aggiungi prenotazione',
'reservations.import.title': 'Importa conferme di prenotazione',
'reservations.import.cta': 'Importa da file',
'reservations.import.dropHere': 'Trascina i file di conferma prenotazione qui o clicca per selezionare',
'reservations.import.dropActive': 'Rilascia i file per importare',
'reservations.import.acceptedFormats': 'Accettati: EML, PDF, PKPass, HTML, TXT (max 10 MB ciascuno, fino a 5 file)',
'reservations.import.parsing': 'Analisi dei file in corso…',
'reservations.import.previewHeading': '{count} prenotazione/i trovata/e',
'reservations.import.previewEmpty': 'Nessuna prenotazione è stata estratta dai file caricati.',
'reservations.import.removeItem': 'Rimuovi',
'reservations.import.confirm': 'Importa {count} prenotazione/i',
'reservations.import.back': 'Indietro',
'reservations.import.success': '{count} prenotazione/i importata/e',
'reservations.import.partialFailure': '{created} importata/e, {failed} fallita/e',
'reservations.import.error': "Analisi fallita. Assicurati che il file sia una conferma di prenotazione valida.",
'reservations.import.unavailable': "L'importazione di prenotazioni non è disponibile su questo server.",
'reservations.import.unsupportedFormat': 'Formato file non supportato. Usa EML, PDF, PKPass, HTML o TXT.',
'reservations.import.fileTooLarge': 'Il file "{name}" supera il limite di 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Importazione Naver Maps',
'undo.addPlace': 'Luogo aggiunto',
'undo.done': 'Annullato: {action}',
'undo.importBooking': 'Importazione conferma prenotazione',
};
export default undo;
+17
View File
@@ -117,5 +117,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'終了日時は開始日時より後である必要があります',
'reservations.addBooking': '予約を追加',
'reservations.import.title': '予約確認書のインポート',
'reservations.import.cta': 'ファイルからインポート',
'reservations.import.dropHere': '予約確認ファイルをここにドロップするか、クリックして選択',
'reservations.import.dropActive': 'ファイルをドロップしてインポート',
'reservations.import.acceptedFormats': '対応形式:EML、PDF、PKPass、HTML、TXT(各最大 10 MB、最大 5 ファイル)',
'reservations.import.parsing': 'ファイルを解析中…',
'reservations.import.previewHeading': '{count} 件の予約が見つかりました',
'reservations.import.previewEmpty': 'アップロードされたファイルから予約を抽出できませんでした。',
'reservations.import.removeItem': '削除',
'reservations.import.confirm': '{count} 件の予約をインポート',
'reservations.import.back': '戻る',
'reservations.import.success': '{count} 件の予約をインポートしました',
'reservations.import.partialFailure': '{created} 件インポート済み、{failed} 件失敗',
'reservations.import.error': '解析に失敗しました。ファイルが有効な予約確認書であることを確認してください。',
'reservations.import.unavailable': 'このサーバーでは予約インポート機能が利用できません。',
'reservations.import.unsupportedFormat': '対応していないファイル形式です。EML、PDF、PKPass、HTML、または TXT を使用してください。',
'reservations.import.fileTooLarge': 'ファイル「{name}」は 10 MB の制限を超えています。',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naverマップをインポート',
'undo.addPlace': '場所を追加',
'undo.done': '元に戻しました: {action}',
'undo.importBooking': '予約確認書インポート',
};
export default undo;
+17
View File
@@ -117,5 +117,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'종료 날짜/시간은 시작 날짜/시간 이후여야 합니다',
'reservations.addBooking': '예약 추가',
'reservations.import.title': '예약 확인서 가져오기',
'reservations.import.cta': '파일에서 가져오기',
'reservations.import.dropHere': '예약 확인 파일을 여기에 끌어다 놓거나 클릭하여 선택',
'reservations.import.dropActive': '가져올 파일을 여기에 놓으세요',
'reservations.import.acceptedFormats': '허용 형식: EML, PDF, PKPass, HTML, TXT (파일당 최대 10 MB, 최대 5개)',
'reservations.import.parsing': '파일 분석 중…',
'reservations.import.previewHeading': '{count}개 예약 발견',
'reservations.import.previewEmpty': '업로드된 파일에서 예약을 추출할 수 없었습니다.',
'reservations.import.removeItem': '제거',
'reservations.import.confirm': '{count}개 예약 가져오기',
'reservations.import.back': '뒤로',
'reservations.import.success': '{count}개 예약을 가져왔습니다',
'reservations.import.partialFailure': '{created}개 가져옴, {failed}개 실패',
'reservations.import.error': '분석 실패. 파일이 유효한 예약 확인서인지 확인하세요.',
'reservations.import.unavailable': '이 서버에서는 예약 가져오기를 사용할 수 없습니다.',
'reservations.import.unsupportedFormat': '지원하지 않는 파일 형식입니다. EML, PDF, PKPass, HTML 또는 TXT를 사용하세요.',
'reservations.import.fileTooLarge': '파일 "{name}"이(가) 10 MB 제한을 초과합니다.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': '네이버 지도 가져오기',
'undo.addPlace': '장소가 추가되었습니다',
'undo.done': '실행 취소됨: {action}',
'undo.importBooking': '예약 확인서 가져오기',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Einddatum/-tijd moet na de startdatum/-tijd liggen',
'reservations.addBooking': 'Boeking toevoegen',
'reservations.import.title': 'Boekingsbevestigingen importeren',
'reservations.import.cta': 'Importeren vanuit bestand',
'reservations.import.dropHere': 'Zet hier bevestigingsbestanden neer of klik om te selecteren',
'reservations.import.dropActive': 'Laat bestanden los om te importeren',
'reservations.import.acceptedFormats': 'Geaccepteerd: EML, PDF, PKPass, HTML, TXT (max. 10 MB per stuk, tot 5 bestanden)',
'reservations.import.parsing': 'Bestanden verwerken…',
'reservations.import.previewHeading': '{count} reservering(en) gevonden',
'reservations.import.previewEmpty': 'Er konden geen reserveringen worden geëxtraheerd uit de geüploade bestanden.',
'reservations.import.removeItem': 'Verwijderen',
'reservations.import.confirm': '{count} reservering(en) importeren',
'reservations.import.back': 'Terug',
'reservations.import.success': '{count} reservering(en) geïmporteerd',
'reservations.import.partialFailure': '{created} geïmporteerd, {failed} mislukt',
'reservations.import.error': 'Verwerking mislukt. Zorg ervoor dat het bestand een geldige boekingsbevestiging is.',
'reservations.import.unavailable': 'Boeking importeren is niet beschikbaar op deze server.',
'reservations.import.unsupportedFormat': 'Niet-ondersteund bestandsformaat. Gebruik EML, PDF, PKPass, HTML of TXT.',
'reservations.import.fileTooLarge': 'Bestand "{name}" overschrijdt de limiet van 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naver Maps-import',
'undo.addPlace': 'Locatie toegevoegd',
'undo.done': 'Ongedaan gemaakt: {action}',
'undo.importBooking': 'Boekingsbevestiging importeren',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Data/godzina zakończenia musi być późniejsza niż data/godzina rozpoczęcia',
'reservations.addBooking': 'Dodaj rezerwację',
'reservations.import.title': 'Importuj potwierdzenia rezerwacji',
'reservations.import.cta': 'Importuj z pliku',
'reservations.import.dropHere': 'Upuść pliki potwierdzeń rezerwacji tutaj lub kliknij, aby wybrać',
'reservations.import.dropActive': 'Upuść pliki, aby zaimportować',
'reservations.import.acceptedFormats': 'Akceptowane: EML, PDF, PKPass, HTML, TXT (maks. 10 MB każdy, do 5 plików)',
'reservations.import.parsing': 'Przetwarzanie plików…',
'reservations.import.previewHeading': 'Znaleziono {count} rezerwację/rezerwacje',
'reservations.import.previewEmpty': 'Nie udało się wyodrębnić rezerwacji z przesłanych plików.',
'reservations.import.removeItem': 'Usuń',
'reservations.import.confirm': 'Importuj {count} rezerwację/rezerwacje',
'reservations.import.back': 'Wstecz',
'reservations.import.success': 'Zaimportowano {count} rezerwację/rezerwacje',
'reservations.import.partialFailure': '{created} zaimportowano, {failed} nieudane',
'reservations.import.error': 'Przetwarzanie nieudane. Upewnij się, że plik jest prawidłowym potwierdzeniem rezerwacji.',
'reservations.import.unavailable': 'Import rezerwacji nie jest dostępny na tym serwerze.',
'reservations.import.unsupportedFormat': 'Nieobsługiwany format pliku. Użyj EML, PDF, PKPass, HTML lub TXT.',
'reservations.import.fileTooLarge': 'Plik „{name}" przekracza limit 10 MB.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Import Naver Maps',
'undo.addPlace': 'Miejsce dodane',
'undo.done': 'Cofnięto: {action}',
'undo.importBooking': 'Import potwierdzenia rezerwacji',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Дата/время окончания должны быть позже даты/времени начала',
'reservations.addBooking': 'Добавить бронирование',
'reservations.import.title': 'Импорт подтверждений бронирования',
'reservations.import.cta': 'Импортировать из файла',
'reservations.import.dropHere': 'Перетащите файлы подтверждений бронирования сюда или нажмите для выбора',
'reservations.import.dropActive': 'Отпустите файлы для импорта',
'reservations.import.acceptedFormats': 'Принимаются: EML, PDF, PKPass, HTML, TXT (макс. 10 МБ каждый, до 5 файлов)',
'reservations.import.parsing': 'Обработка файлов…',
'reservations.import.previewHeading': 'Найдено {count} бронирование(й)',
'reservations.import.previewEmpty': 'Из загруженных файлов не удалось извлечь бронирования.',
'reservations.import.removeItem': 'Удалить',
'reservations.import.confirm': 'Импортировать {count} бронирование(й)',
'reservations.import.back': 'Назад',
'reservations.import.success': '{count} бронирование(й) импортировано',
'reservations.import.partialFailure': '{created} импортировано, {failed} не удалось',
'reservations.import.error': 'Обработка не удалась. Убедитесь, что файл является действительным подтверждением бронирования.',
'reservations.import.unavailable': 'Импорт бронирований недоступен на этом сервере.',
'reservations.import.unsupportedFormat': 'Неподдерживаемый формат файла. Используйте EML, PDF, PKPass, HTML или TXT.',
'reservations.import.fileTooLarge': 'Файл «{name}» превышает ограничение в 10 МБ.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Импорт из Naver Maps',
'undo.addPlace': 'Место добавлено',
'undo.done': 'Отменено: {action}',
'undo.importBooking': 'Импорт подтверждения бронирования',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Bitiş tarihi/saati başlangıçtan sonra olmalı',
'reservations.addBooking': 'Rezervasyon ekle',
'reservations.import.title': 'Rezervasyon onaylarını içe aktar',
'reservations.import.cta': 'Dosyadan içe aktar',
'reservations.import.dropHere': 'Rezervasyon onay dosyalarını buraya sürükleyin veya seçmek için tıklayın',
'reservations.import.dropActive': 'İçe aktarmak için dosyaları bırakın',
'reservations.import.acceptedFormats': 'Kabul edilenler: EML, PDF, PKPass, HTML, TXT (her biri maks. 10 MB, en fazla 5 dosya)',
'reservations.import.parsing': 'Dosyalar işleniyor…',
'reservations.import.previewHeading': '{count} rezervasyon bulundu',
'reservations.import.previewEmpty': 'Yüklenen dosyalardan hiçbir rezervasyon çıkarılamadı.',
'reservations.import.removeItem': 'Kaldır',
'reservations.import.confirm': '{count} rezervasyonu içe aktar',
'reservations.import.back': 'Geri',
'reservations.import.success': '{count} rezervasyon içe aktarıldı',
'reservations.import.partialFailure': '{created} içe aktarıldı, {failed} başarısız',
'reservations.import.error': 'İşlem başarısız. Dosyanın geçerli bir rezervasyon onayı olduğundan emin olun.',
'reservations.import.unavailable': 'Rezervasyon içe aktarma bu sunucuda mevcut değil.',
'reservations.import.unsupportedFormat': 'Desteklenmeyen dosya biçimi. EML, PDF, PKPass, HTML veya TXT kullanın.',
'reservations.import.fileTooLarge': '"{name}" dosyası 10 MB sınırını aşıyor.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naver Haritalar içe aktarma',
'undo.addPlace': 'Yer eklendi',
'undo.done': 'Geri alındı: {action}',
'undo.importBooking': 'Rezervasyon onayı içe aktarma',
};
export default undo;
+17
View File
@@ -120,5 +120,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'Дата/час закінчення повинен бути пізніше дати/часу початку',
'reservations.addBooking': 'Добавить бронирование',
'reservations.import.title': 'Імпорт підтверджень бронювання',
'reservations.import.cta': 'Імпортувати з файлу',
'reservations.import.dropHere': 'Перетягніть файли підтверджень бронювання сюди або натисніть для вибору',
'reservations.import.dropActive': 'Відпустіть файли для імпорту',
'reservations.import.acceptedFormats': 'Підтримуються: EML, PDF, PKPass, HTML, TXT (макс. 10 МБ кожен, до 5 файлів)',
'reservations.import.parsing': 'Обробка файлів…',
'reservations.import.previewHeading': 'Знайдено {count} бронювання(нь)',
'reservations.import.previewEmpty': 'З завантажених файлів не вдалося витягти бронювання.',
'reservations.import.removeItem': 'Видалити',
'reservations.import.confirm': 'Імпортувати {count} бронювання(нь)',
'reservations.import.back': 'Назад',
'reservations.import.success': '{count} бронювання(нь) імпортовано',
'reservations.import.partialFailure': '{created} імпортовано, {failed} не вдалося',
'reservations.import.error': 'Обробка не вдалася. Переконайтесь, що файл є дійсним підтвердженням бронювання.',
'reservations.import.unavailable': 'Імпорт бронювань недоступний на цьому сервері.',
'reservations.import.unsupportedFormat': 'Непідтримуваний формат файлу. Використовуйте EML, PDF, PKPass, HTML або TXT.',
'reservations.import.fileTooLarge': 'Файл «{name}» перевищує обмеження в 10 МБ.',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Імпорт з Naver Maps',
'undo.addPlace': 'Місце додано',
'undo.done': 'Відмінено: {action}',
'undo.importBooking': 'Імпорт підтвердження бронювання',
};
export default undo;
+17
View File
@@ -116,5 +116,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'結束日期/時間必須晚於開始日期/時間',
'reservations.addBooking': '新增預訂',
'reservations.import.title': '匯入訂位確認',
'reservations.import.cta': '從檔案匯入',
'reservations.import.dropHere': '將訂位確認檔案拖放到此處,或點擊選擇',
'reservations.import.dropActive': '放開檔案以匯入',
'reservations.import.acceptedFormats': '支援格式:EML、PDF、PKPass、HTML、TXT(每個最大 10 MB,最多 5 個檔案)',
'reservations.import.parsing': '正在解析檔案…',
'reservations.import.previewHeading': '找到 {count} 筆預訂',
'reservations.import.previewEmpty': '無法從上傳的檔案中提取任何預訂資訊。',
'reservations.import.removeItem': '移除',
'reservations.import.confirm': '匯入 {count} 筆預訂',
'reservations.import.back': '返回',
'reservations.import.success': '已匯入 {count} 筆預訂',
'reservations.import.partialFailure': '已匯入 {created} 筆,{failed} 筆失敗',
'reservations.import.error': '解析失敗。請確保檔案是有效的訂位確認。',
'reservations.import.unavailable': '此伺服器上的預訂匯入功能不可用。',
'reservations.import.unsupportedFormat': '不支援的檔案格式。請使用 EML、PDF、PKPass、HTML 或 TXT。',
'reservations.import.fileTooLarge': '檔案「{name}」超過 10 MB 限制。',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naver 地圖匯入',
'undo.addPlace': '地點已新增',
'undo.done': '已撤銷:{action}',
'undo.importBooking': '匯入訂位確認',
};
export default undo;
+17
View File
@@ -116,5 +116,22 @@ const reservations: TranslationStrings = {
'reservations.validation.endBeforeStart':
'结束日期/时间必须晚于开始日期/时间',
'reservations.addBooking': '添加预订',
'reservations.import.title': '导入预订确认',
'reservations.import.cta': '从文件导入',
'reservations.import.dropHere': '将预订确认文件拖放到此处,或点击选择',
'reservations.import.dropActive': '松开文件以导入',
'reservations.import.acceptedFormats': '支持格式:EML、PDF、PKPass、HTML、TXT(每个最大 10 MB,最多 5 个文件)',
'reservations.import.parsing': '正在解析文件…',
'reservations.import.previewHeading': '找到 {count} 个预订',
'reservations.import.previewEmpty': '无法从上传的文件中提取任何预订信息。',
'reservations.import.removeItem': '移除',
'reservations.import.confirm': '导入 {count} 个预订',
'reservations.import.back': '返回',
'reservations.import.success': '已导入 {count} 个预订',
'reservations.import.partialFailure': '已导入 {created} 个,{failed} 个失败',
'reservations.import.error': '解析失败。请确保文件是有效的预订确认。',
'reservations.import.unavailable': '此服务器上的预订导入功能不可用。',
'reservations.import.unsupportedFormat': '不支持的文件格式。请使用 EML、PDF、PKPass、HTML 或 TXT。',
'reservations.import.fileTooLarge': '文件"{name}"超过 10 MB 限制。',
};
export default reservations;
+1
View File
@@ -17,5 +17,6 @@ const undo: TranslationStrings = {
'undo.importNaverList': 'Naver 地图导入',
'undo.addPlace': '地点已添加',
'undo.done': '已撤销:{action}',
'undo.importBooking': '导入预订确认',
};
export default undo;
@@ -141,3 +141,66 @@ export const accommodationUpdateRequestSchema = open;
export type AccommodationUpdateRequest = z.infer<
typeof accommodationUpdateRequestSchema
>;
// ---------------------------------------------------------------------------
// Booking import (KItinerary)
// ---------------------------------------------------------------------------
const bookingImportEndpointSchema = z.object({
role: z.enum(['from', 'to', 'stop']),
sequence: z.number(),
name: z.string(),
code: z.string().nullable(),
lat: z.number(),
lng: z.number(),
timezone: z.string().nullable(),
local_time: z.string().nullable(),
local_date: z.string().nullable(),
});
const bookingImportVenueSchema = z.object({
name: z.string(),
lat: z.number().optional(),
lng: z.number().optional(),
address: z.string().optional(),
website: z.string().optional(),
phone: z.string().optional(),
});
const bookingImportAccommodationSchema = z.object({
check_in: z.string().optional(),
check_out: z.string().optional(),
confirmation: z.string().optional(),
});
export const bookingImportPreviewItemSchema = z.object({
type: z.string(),
title: z.string().min(1),
reservation_time: z.string().nullable().optional(),
reservation_end_time: z.string().nullable().optional(),
confirmation_number: z.string().nullable().optional(),
location: z.string().nullable().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
endpoints: z.array(bookingImportEndpointSchema).optional(),
needs_review: z.boolean().optional(),
_venue: bookingImportVenueSchema.optional(),
_accommodation: bookingImportAccommodationSchema.optional(),
source: z.object({ fileName: z.string(), index: z.number() }),
});
export type BookingImportPreviewItem = z.infer<typeof bookingImportPreviewItemSchema>;
export const bookingImportPreviewResponseSchema = z.object({
items: z.array(bookingImportPreviewItemSchema),
warnings: z.array(z.string()),
});
export type BookingImportPreviewResponse = z.infer<typeof bookingImportPreviewResponseSchema>;
export const bookingImportConfirmRequestSchema = z.object({
items: z.array(bookingImportPreviewItemSchema).min(1),
});
export type BookingImportConfirmRequest = z.infer<typeof bookingImportConfirmRequestSchema>;
export const bookingImportConfirmResponseSchema = z.object({
created: z.array(reservationSchema),
});
export type BookingImportConfirmResponse = z.infer<typeof bookingImportConfirmResponseSchema>;