fix(journey): resilient per-file photo upload with retry and progress

Replace the single combined-FormData POST (which timed out at 8 s on slow
connections) with a concurrent per-file upload queue: each file is uploaded
individually with timeout disabled, up to 2 retries with backoff (4xx errors
are not retried), and a stable idempotency key per file so server-side dedup
prevents duplicates on retry. Partial success is preserved — succeeded photos
merge into state immediately; failed files stay queued for re-save. Progress
is surfaced as "Uploading n/N" in both the entry editor and gallery upload
button. Adds translations for two new keys across all 15 locales.

Closes #1013
This commit is contained in:
jubnl
2026-05-22 15:17:23 +02:00
parent 8f010e38cf
commit 817b9a5357
20 changed files with 277 additions and 49 deletions
+2
View File
@@ -1713,6 +1713,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'فشل رفع الصور',
'journey.editor.uploadPhotos': 'رفع صور',
'journey.editor.uploading': '...جارٍ الرفع',
'journey.editor.uploadingProgress': 'جارٍ الرفع {done}/{total}…',
'journey.editor.uploadPartialFailed': 'فشل رفع {failed} من {total} — احفظ مجدداً للمحاولة',
'journey.editor.fromGallery': 'من المعرض',
'journey.editor.addAnother': 'إضافة آخر',
'journey.editor.makeFirst': 'جعله الأول',
+2
View File
@@ -2084,6 +2084,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Falha ao enviar fotos',
'journey.editor.uploadPhotos': 'Enviar fotos',
'journey.editor.uploading': 'Enviando...',
'journey.editor.uploadingProgress': 'Enviando {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} de {total} fotos falharam — salve novamente para tentar',
'journey.editor.fromGallery': 'Da galeria',
'journey.editor.allPhotosAdded': 'Todas as fotos já foram adicionadas',
'journey.editor.writeStory': 'Escreva sua história...',
+2
View File
@@ -2089,6 +2089,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Nahrávání fotek selhalo',
'journey.editor.uploadPhotos': 'Nahrát fotky',
'journey.editor.uploading': 'Nahrávání...',
'journey.editor.uploadingProgress': 'Nahrávání {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} z {total} fotek selhalo — uložte znovu pro opakování',
'journey.editor.fromGallery': 'Z galerie',
'journey.editor.allPhotosAdded': 'Všechny fotky již přidány',
'journey.editor.writeStory': 'Napište svůj příběh...',
+2
View File
@@ -2092,6 +2092,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Foto-Upload fehlgeschlagen',
'journey.editor.uploadPhotos': 'Fotos hochladen',
'journey.editor.uploading': 'Hochladen...',
'journey.editor.uploadingProgress': 'Hochladen {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} von {total} Fotos fehlgeschlagen — erneut speichern zum Wiederholen',
'journey.editor.fromGallery': 'Aus Galerie',
'journey.editor.allPhotosAdded': 'Alle Fotos bereits hinzugefügt',
'journey.editor.writeStory': 'Erzähle deine Geschichte...',
+2
View File
@@ -2118,6 +2118,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Photo upload failed',
'journey.editor.uploadPhotos': 'Upload photos',
'journey.editor.uploading': 'Uploading...',
'journey.editor.uploadingProgress': 'Uploading {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} of {total} photos failed — save again to retry',
'journey.editor.fromGallery': 'From Gallery',
'journey.editor.allPhotosAdded': 'All photos already added',
'journey.editor.writeStory': 'Write your story...',
+2
View File
@@ -2091,6 +2091,8 @@ const es: Record<string, string> = {
'journey.editor.uploadFailed': 'Error al subir fotos',
'journey.editor.uploadPhotos': 'Subir fotos',
'journey.editor.uploading': 'Subiendo...',
'journey.editor.uploadingProgress': 'Subiendo {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} de {total} fotos fallaron — guarda de nuevo para reintentar',
'journey.editor.fromGallery': 'Desde galería',
'journey.editor.allPhotosAdded': 'Todas las fotos ya fueron añadidas',
'journey.editor.writeStory': 'Escribe tu historia...',
+2
View File
@@ -2085,6 +2085,8 @@ const fr: Record<string, string> = {
'journey.editor.uploadFailed': 'Échec du téléversement des photos',
'journey.editor.uploadPhotos': 'Téléverser des photos',
'journey.editor.uploading': 'Envoi...',
'journey.editor.uploadingProgress': 'Téléversement {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} sur {total} photos ont échoué — sauvegardez à nouveau pour réessayer',
'journey.editor.fromGallery': 'Depuis la galerie',
'journey.editor.allPhotosAdded': 'Toutes les photos ont déjà été ajoutées',
'journey.editor.writeStory': 'Écrivez votre histoire...',
+2
View File
@@ -2086,6 +2086,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'A fotók feltöltése sikertelen',
'journey.editor.uploadPhotos': 'Fotók feltöltése',
'journey.editor.uploading': 'Feltöltés...',
'journey.editor.uploadingProgress': 'Feltöltés {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} / {total} fotó sikertelen — mentsd el újra a próbálkozáshoz',
'journey.editor.fromGallery': 'Galériából',
'journey.editor.allPhotosAdded': 'Minden fotó már hozzáadva',
'journey.editor.writeStory': 'Írd meg a történeted...',
+2
View File
@@ -2101,6 +2101,8 @@ const id: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Gagal mengunggah foto',
'journey.editor.uploadPhotos': 'Unggah foto',
'journey.editor.uploading': 'Mengunggah...',
'journey.editor.uploadingProgress': 'Mengunggah {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} dari {total} foto gagal — simpan lagi untuk mencoba ulang',
'journey.editor.fromGallery': 'Dari Galeri',
'journey.editor.allPhotosAdded': 'Semua foto sudah ditambahkan',
'journey.editor.writeStory': 'Tulis kisahmu...',
+2
View File
@@ -2086,6 +2086,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Caricamento foto non riuscito',
'journey.editor.uploadPhotos': 'Carica foto',
'journey.editor.uploading': 'Caricamento...',
'journey.editor.uploadingProgress': 'Caricamento {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} di {total} foto non riuscite — salva di nuovo per riprovare',
'journey.editor.fromGallery': 'Dalla galleria',
'journey.editor.allPhotosAdded': 'Tutte le foto sono già state aggiunte',
'journey.editor.writeStory': 'Scrivi la tua storia...',
+2
View File
@@ -2085,6 +2085,8 @@ const nl: Record<string, string> = {
'journey.editor.uploadFailed': 'Foto uploaden mislukt',
'journey.editor.uploadPhotos': 'Foto\'s uploaden',
'journey.editor.uploading': 'Uploaden...',
'journey.editor.uploadingProgress': 'Uploaden {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} van {total} foto\'s mislukt — sla opnieuw op om het opnieuw te proberen',
'journey.editor.fromGallery': 'Uit galerij',
'journey.editor.allPhotosAdded': 'Alle foto\'s al toegevoegd',
'journey.editor.writeStory': 'Schrijf je verhaal...',
+2
View File
@@ -2078,6 +2078,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'journey.editor.uploadFailed': 'Przesyłanie zdjęć nie powiodło się',
'journey.editor.uploadPhotos': 'Prześlij zdjęcia',
'journey.editor.uploading': 'Przesyłanie...',
'journey.editor.uploadingProgress': 'Przesyłanie {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} z {total} zdjęć nie powiodło się — zapisz ponownie, aby spróbować',
'journey.editor.fromGallery': 'Z galerii',
'journey.editor.allPhotosAdded': 'Wszystkie zdjęcia już dodane',
'journey.editor.writeStory': 'Napisz swoją historię...',
+2
View File
@@ -2085,6 +2085,8 @@ const ru: Record<string, string> = {
'journey.editor.uploadFailed': 'Не удалось загрузить фото',
'journey.editor.uploadPhotos': 'Загрузить фото',
'journey.editor.uploading': 'Загрузка...',
'journey.editor.uploadingProgress': 'Загрузка {done}/{total}…',
'journey.editor.uploadPartialFailed': '{failed} из {total} фото не удалось загрузить — сохраните снова для повтора',
'journey.editor.fromGallery': 'Из галереи',
'journey.editor.allPhotosAdded': 'Все фото уже добавлены',
'journey.editor.writeStory': 'Напишите свою историю...',
+2
View File
@@ -2085,6 +2085,8 @@ const zh: Record<string, string> = {
'journey.editor.uploadFailed': '照片上传失败',
'journey.editor.uploadPhotos': '上传照片',
'journey.editor.uploading': '上传中...',
'journey.editor.uploadingProgress': '上传中 {done}/{total}…',
'journey.editor.uploadPartialFailed': '{total} 张中有 {failed} 张上传失败 — 再次保存以重试',
'journey.editor.fromGallery': '从相册',
'journey.editor.allPhotosAdded': '所有照片已添加',
'journey.editor.writeStory': '写下你的故事...',
+2
View File
@@ -2043,6 +2043,8 @@ const zhTw: Record<string, string> = {
'journey.editor.uploadFailed': '照片上傳失敗',
'journey.editor.uploadPhotos': '上傳照片',
'journey.editor.uploading': '上傳中...',
'journey.editor.uploadingProgress': '上傳中 {done}/{total}…',
'journey.editor.uploadPartialFailed': '{total} 張中有 {failed} 張上傳失敗 — 再次儲存以重試',
'journey.editor.fromGallery': '從相簿',
'journey.editor.allPhotosAdded': '所有照片已新增',
'journey.editor.writeStory': '寫下你的故事...',