setShowClearConfirm(true)}>
diff --git a/client/src/sync/mutationQueue.ts b/client/src/sync/mutationQueue.ts
index 6eb4a0784..1d013b0d4 100644
--- a/client/src/sync/mutationQueue.ts
+++ b/client/src/sync/mutationQueue.ts
@@ -66,6 +66,14 @@ export function nextTempId(): number {
return -_lastTempId
}
+/**
+ * A write the server answers with a 5xx this many times is parked as failed.
+ * The gaps double from 30 s, so the last try comes about two hours after the
+ * first: long enough for a restart or an outage, short of blocking forever.
+ */
+const MAX_SERVER_ERROR_ATTEMPTS = 8
+const SERVER_ERROR_BACKOFF_MS = 30_000
+
/** HTTP statuses that should be retried later rather than treated as terminal. */
function isRetryableStatus(status: number | undefined): boolean {
// 401: token expired mid-flush (offline window) — retry after re-auth.
@@ -112,8 +120,10 @@ export const mutationQueue = {
/**
* Drain the queue: replay each pending mutation against the server in FIFO order.
- * Stops on first network error (will retry on next trigger).
- * 4xx responses are marked failed and skipped.
+ * Stops on the first network error (retried on the next trigger). 4xx answers
+ * are marked failed and skipped; a DELETE answered 404 counts as done. A 5xx
+ * holds back only its own trip, retried with growing gaps, and is parked as
+ * failed once it has failed MAX_SERVER_ERROR_ATTEMPTS times.
*/
async flush(): Promise {
if (_flushing || isEffectivelyOffline() || !isAuthed()) return
@@ -145,12 +155,21 @@ export const mutationQueue = {
.equals('pending')
.sortBy('createdAt')
+ // Trips whose queue waits behind a write the server failed on. Their later
+ // writes must keep their order, but every other trip goes on syncing.
+ const blockedTrips = new Set()
+
for (const mutation of pending) {
// Re-checked every pass, not just on entry: this loop writes server
// responses straight into Dexie, and after a logout the proxy points at
// the shared anonymous database. A flush that started before logout
// would otherwise seed it with the previous account's rows.
if (!isAuthed()) break
+ if (blockedTrips.has(mutation.tripId)) continue
+ if (mutation.retryAfter !== undefined && mutation.retryAfter > Date.now()) {
+ blockedTrips.add(mutation.tripId)
+ continue
+ }
// Mark as syncing so UI can show progress. The stamp is what lets the
// next flush tell an in-flight row from one a killed tab abandoned.
@@ -263,6 +282,17 @@ export const mutationQueue = {
} catch (err: unknown) {
const httpStatus = (err as { response?: { status: number } })?.response?.status
+ // A DELETE of something the server no longer has did what it set out
+ // to do. Reporting it as a failed sync would flag a change that worked.
+ if (httpStatus === 404 && mutation.method === 'DELETE') {
+ if (mutation.resource && reqEntityId !== undefined) {
+ const table = getTable(mutation.resource)
+ if (table) await table.delete(reqEntityId)
+ }
+ await offlineDb.mutationQueue.delete(mutation.id)
+ continue
+ }
+
// 409 = the entity changed on the server since this offline edit was
// made. This is NOT a dropped change like other 4xx — resolve it per
// the user's strategy instead of failing it. Deliberately scoped to
@@ -307,8 +337,29 @@ export const mutationQueue = {
attempts: mutation.attempts + 1,
lastError: String(err),
})
+ } else if (httpStatus !== undefined && httpStatus >= 500) {
+ // The server answered and failed. Retried with growing gaps, and
+ // only this trip waits for it. One that never goes through is
+ // parked as failed instead of holding every later write forever.
+ const attempts = mutation.attempts + 1
+ if (attempts >= MAX_SERVER_ERROR_ATTEMPTS) {
+ if (mutation.method !== 'DELETE' && mutation.tempId !== undefined && mutation.resource) {
+ const table = getTable(mutation.resource)
+ if (table) await table.delete(mutation.tempId)
+ }
+ await offlineDb.mutationQueue.update(mutation.id, { status: 'failed', attempts, lastError: String(err), retryAfter: undefined })
+ } else {
+ await offlineDb.mutationQueue.update(mutation.id, {
+ status: 'pending',
+ attempts,
+ lastError: String(err),
+ retryAfter: Date.now() + SERVER_ERROR_BACKOFF_MS * 2 ** (attempts - 1),
+ })
+ blockedTrips.add(mutation.tripId)
+ }
} else {
- // Network / transient error — reset to pending, abort flush (retry next trigger)
+ // No answer at all (network) or a retryable status — reset to
+ // pending and stop the flush; the next trigger retries.
await offlineDb.mutationQueue.update(mutation.id, {
status: 'pending',
attempts: mutation.attempts + 1,
@@ -364,6 +415,23 @@ export const mutationQueue = {
.count()
},
+ /** Put every parked change back in line, from a fresh start, and flush. */
+ async retryFailed(): Promise {
+ await offlineDb.mutationQueue
+ .where('status')
+ .equals('failed')
+ .modify(m => { m.status = 'pending'; m.attempts = 0; m.retryAfter = undefined; m.lastError = null })
+ await this.flush()
+ },
+
+ /**
+ * Drop every parked change. The local copy of an edit that never reached the
+ * server goes with the next trip sync, which reads the server's version.
+ */
+ async discardFailed(): Promise {
+ await offlineDb.mutationQueue.where('status').equals('failed').delete()
+ },
+
/** Count unresolved sync conflicts (offline edits the server rejected as stale). */
async conflictCount(): Promise {
return offlineDb.mutationQueue
diff --git a/client/tests/unit/sync/mutationQueue.test.ts b/client/tests/unit/sync/mutationQueue.test.ts
index 19cfc6a5d..5cd3a7973 100644
--- a/client/tests/unit/sync/mutationQueue.test.ts
+++ b/client/tests/unit/sync/mutationQueue.test.ts
@@ -503,3 +503,89 @@ describe('mutationQueue.flush — failure handling (B3)', () => {
expect(m!.status).toBe('pending');
});
});
+
+describe('mutationQueue.flush — a write the server keeps failing on', () => {
+ it('holds back only its own trip, with a growing gap, while other trips sync', async () => {
+ const stuck = generateUUID();
+ const sameTrip = generateUUID();
+ const otherTrip = generateUUID();
+ await mutationQueue.enqueue(makeMutation({ id: stuck }));
+ await mutationQueue.enqueue(makeMutation({ id: sameTrip, body: { name: 'Louvre' } }));
+ await mutationQueue.enqueue(makeMutation({ id: otherTrip, tripId: 2, url: '/trips/2/places' }));
+
+ server.use(
+ http.post('/api/trips/1/places', () => HttpResponse.json({ error: 'boom' }, { status: 500 })),
+ http.post('/api/trips/2/places', () => HttpResponse.json({ place: buildPlace({ trip_id: 2, id: 77 }) })),
+ );
+
+ const before = Date.now();
+ await mutationQueue.flush();
+
+ const first = await offlineDb.mutationQueue.get(stuck);
+ expect(first).toMatchObject({ status: 'pending', attempts: 1 });
+ expect(first!.retryAfter).toBeGreaterThanOrEqual(before + 30_000);
+ // The trip's later write keeps its place in line…
+ expect(await offlineDb.mutationQueue.get(sameTrip)).toMatchObject({ status: 'pending', attempts: 0 });
+ // …and the other trip is not held up at all.
+ expect(await offlineDb.mutationQueue.get(otherTrip)).toBeUndefined();
+
+ // Inside the gap the next trigger does not knock again.
+ await mutationQueue.flush();
+ expect((await offlineDb.mutationQueue.get(stuck))!.attempts).toBe(1);
+ });
+
+ it('parks it as failed once it has failed often enough, so the trip moves on', async () => {
+ const stuck = generateUUID();
+ const tempId = nextTempId();
+ await offlineDb.places.put(buildPlace({ trip_id: 1, id: tempId }));
+ await mutationQueue.enqueue(makeMutation({ id: stuck, tempId }));
+ await offlineDb.mutationQueue.update(stuck, { attempts: 7 });
+
+ server.use(http.post('/api/trips/1/places', () => HttpResponse.json({ error: 'boom' }, { status: 500 })));
+ await mutationQueue.flush();
+
+ expect(await offlineDb.mutationQueue.get(stuck)).toMatchObject({ status: 'failed', attempts: 8 });
+ expect(await offlineDb.places.get(tempId)).toBeUndefined();
+ expect(await mutationQueue.failedCount()).toBe(1);
+ });
+
+ it('counts a DELETE the server answers 404 as done', async () => {
+ const id = generateUUID();
+ await offlineDb.places.put(buildPlace({ trip_id: 1, id: 5 }));
+ await mutationQueue.enqueue(makeMutation({ id, method: 'DELETE', url: '/trips/1/places/5', body: undefined, entityId: 5 }));
+
+ server.use(http.delete('/api/trips/1/places/5', () => HttpResponse.json({ error: 'Place not found' }, { status: 404 })));
+ await mutationQueue.flush();
+
+ expect(await offlineDb.mutationQueue.get(id)).toBeUndefined();
+ expect(await offlineDb.places.get(5)).toBeUndefined();
+ expect(await mutationQueue.failedCount()).toBe(0);
+ });
+});
+
+describe('mutationQueue — parked changes', () => {
+ it('retryFailed puts them back in line from a fresh start and sends them', async () => {
+ const id = generateUUID();
+ await mutationQueue.enqueue(makeMutation({ id }));
+ await offlineDb.mutationQueue.update(id, { status: 'failed', attempts: 8, lastError: 'boom' });
+ server.use(http.post('/api/trips/1/places', () => HttpResponse.json({ place: buildPlace({ trip_id: 1, id: 91 }) })));
+
+ await mutationQueue.retryFailed();
+
+ expect(await offlineDb.mutationQueue.get(id)).toBeUndefined();
+ expect(await offlineDb.places.get(91)).toBeDefined();
+ });
+
+ it('discardFailed drops only the parked ones', async () => {
+ const parked = generateUUID();
+ const waiting = generateUUID();
+ await mutationQueue.enqueue(makeMutation({ id: parked }));
+ await mutationQueue.enqueue(makeMutation({ id: waiting }));
+ await offlineDb.mutationQueue.update(parked, { status: 'failed' });
+
+ await mutationQueue.discardFailed();
+
+ expect(await offlineDb.mutationQueue.get(parked)).toBeUndefined();
+ expect(await offlineDb.mutationQueue.get(waiting)).toBeDefined();
+ });
+});
diff --git a/shared/src/i18n/ar/settings.ts b/shared/src/i18n/ar/settings.ts
index 0fde1e045..359297694 100644
--- a/shared/src/i18n/ar/settings.ts
+++ b/shared/src/i18n/ar/settings.ts
@@ -535,6 +535,9 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'الرحلات المخزّنة مؤقتًا',
'settings.offline.stats.pending': 'تغييرات معلّقة',
'settings.offline.stats.failed': 'تغييرات فاشلة',
+ 'settings.offline.failed.hint': 'لم تصل هذه التغييرات إلى الخادم. أعد المحاولة أو تجاهلها للاحتفاظ بنسخة الخادم.',
+ 'settings.offline.failed.retry': 'إعادة المحاولة',
+ 'settings.offline.failed.discard': 'تجاهل',
'settings.offline.stats.conflicts': 'تعارضات',
'settings.offline.empty': 'لا توجد رحلات مخزّنة بعد. اتصل بالإنترنت للمزامنة.',
'settings.offline.loading': 'جارٍ التحميل…',
diff --git a/shared/src/i18n/az/settings.ts b/shared/src/i18n/az/settings.ts
index 60aff0a23..586b79e8c 100644
--- a/shared/src/i18n/az/settings.ts
+++ b/shared/src/i18n/az/settings.ts
@@ -678,6 +678,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Keşlənmiş səyahətlər',
'settings.offline.stats.pending': 'Gözləyən dəyişikliklər',
'settings.offline.stats.failed': 'Uğursuz dəyişikliklər',
+ 'settings.offline.failed.hint':
+ 'Bu dəyişikliklər serverə çatmayıb. Yenidən cəhd edin və ya serverdəki versiyanı saxlamaq üçün onları ləğv edin.',
+ 'settings.offline.failed.retry': 'Yenidən cəhd et',
+ 'settings.offline.failed.discard': 'Ləğv et',
'settings.offline.stats.conflicts': 'Ziddiyyətlər',
'settings.offline.empty':
'Hələ heç bir səyahət keşlənməyib. Sinxronlaşdırmaq üçün internetə qoşulun.',
diff --git a/shared/src/i18n/br/settings.ts b/shared/src/i18n/br/settings.ts
index 19164fee2..c8d2a2e19 100644
--- a/shared/src/i18n/br/settings.ts
+++ b/shared/src/i18n/br/settings.ts
@@ -555,6 +555,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Viagens em cache',
'settings.offline.stats.pending': 'Alterações pendentes',
'settings.offline.stats.failed': 'Alterações com falha',
+ 'settings.offline.failed.hint':
+ 'Essas alterações nunca chegaram ao servidor. Tente novamente ou descarte-as para manter a versão do servidor.',
+ 'settings.offline.failed.retry': 'Tentar novamente',
+ 'settings.offline.failed.discard': 'Descartar',
'settings.offline.stats.conflicts': 'Conflitos',
'settings.offline.empty': 'Nenhuma viagem em cache ainda. Conecte-se à internet para sincronizar.',
'settings.offline.loading': 'Carregando…',
diff --git a/shared/src/i18n/ca/settings.ts b/shared/src/i18n/ca/settings.ts
index a8647cedb..9c15ad4f8 100644
--- a/shared/src/i18n/ca/settings.ts
+++ b/shared/src/i18n/ca/settings.ts
@@ -530,6 +530,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Viatges a la memòria cau',
'settings.offline.stats.pending': 'Canvis pendents',
'settings.offline.stats.failed': 'Canvis fallits',
+ 'settings.offline.failed.hint':
+ 'Aquests canvis no han arribat mai al servidor. Torna-ho a provar o descarta’ls per conservar la versió del servidor.',
+ 'settings.offline.failed.retry': 'Torna-ho a provar',
+ 'settings.offline.failed.discard': 'Descarta',
'settings.offline.stats.conflicts': 'Conflictes',
'settings.offline.empty': "Encara no hi ha cap viatge a la memòria cau. Connecta't a internet per sincronitzar.",
'settings.offline.loading': 'Carregant…',
diff --git a/shared/src/i18n/cs/settings.ts b/shared/src/i18n/cs/settings.ts
index 8f2f6e0d4..65cca6539 100644
--- a/shared/src/i18n/cs/settings.ts
+++ b/shared/src/i18n/cs/settings.ts
@@ -543,6 +543,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Cesty v mezipaměti',
'settings.offline.stats.pending': 'Čekající změny',
'settings.offline.stats.failed': 'Neúspěšné změny',
+ 'settings.offline.failed.hint':
+ 'Tyto změny se na server nikdy nedostaly. Zkuste to znovu, nebo je zahoďte a ponechte verzi ze serveru.',
+ 'settings.offline.failed.retry': 'Zkusit znovu',
+ 'settings.offline.failed.discard': 'Zahodit',
'settings.offline.stats.conflicts': 'Konflikty',
'settings.offline.empty': 'Zatím žádné cesty v mezipaměti. Pro synchronizaci se připojte k internetu.',
'settings.offline.loading': 'Načítání…',
diff --git a/shared/src/i18n/de/settings.ts b/shared/src/i18n/de/settings.ts
index ff7d30738..dfb2c1264 100644
--- a/shared/src/i18n/de/settings.ts
+++ b/shared/src/i18n/de/settings.ts
@@ -557,6 +557,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Zwischengespeicherte Reisen',
'settings.offline.stats.pending': 'Ausstehende Änderungen',
'settings.offline.stats.failed': 'Fehlgeschlagene Änderungen',
+ 'settings.offline.failed.hint':
+ 'Diese Änderungen haben den Server nie erreicht. Versuche es erneut oder verwirf sie, um die Version vom Server zu behalten.',
+ 'settings.offline.failed.retry': 'Erneut versuchen',
+ 'settings.offline.failed.discard': 'Verwerfen',
'settings.offline.stats.conflicts': 'Konflikte',
'settings.offline.empty':
'Noch keine Reisen zwischengespeichert. Verbinde dich mit dem Internet zum Synchronisieren.',
diff --git a/shared/src/i18n/en/settings.ts b/shared/src/i18n/en/settings.ts
index ec4ede372..af771f68c 100644
--- a/shared/src/i18n/en/settings.ts
+++ b/shared/src/i18n/en/settings.ts
@@ -552,6 +552,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Cached trips',
'settings.offline.stats.pending': 'Pending changes',
'settings.offline.stats.failed': 'Failed changes',
+ 'settings.offline.failed.hint':
+ "These changes never reached the server. Try them again, or discard them to keep the server's version.",
+ 'settings.offline.failed.retry': 'Try again',
+ 'settings.offline.failed.discard': 'Discard',
'settings.offline.stats.conflicts': 'Conflicts',
'settings.offline.empty': 'No trips cached yet. Connect to the internet to sync.',
'settings.offline.loading': 'Loading…',
diff --git a/shared/src/i18n/es/settings.ts b/shared/src/i18n/es/settings.ts
index 24dec908c..691b4957a 100644
--- a/shared/src/i18n/es/settings.ts
+++ b/shared/src/i18n/es/settings.ts
@@ -555,6 +555,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Viajes en caché',
'settings.offline.stats.pending': 'Cambios pendientes',
'settings.offline.stats.failed': 'Cambios fallidos',
+ 'settings.offline.failed.hint':
+ 'Estos cambios nunca llegaron al servidor. Vuelve a intentarlo o descártalos para conservar la versión del servidor.',
+ 'settings.offline.failed.retry': 'Reintentar',
+ 'settings.offline.failed.discard': 'Descartar',
'settings.offline.stats.conflicts': 'Conflictos',
'settings.offline.empty': 'Aún no hay viajes en caché. Conéctate a internet para sincronizar.',
'settings.offline.loading': 'Cargando…',
diff --git a/shared/src/i18n/et/settings.ts b/shared/src/i18n/et/settings.ts
index f1997f13a..7f174cc1f 100644
--- a/shared/src/i18n/et/settings.ts
+++ b/shared/src/i18n/et/settings.ts
@@ -561,6 +561,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Vahemälus reisid',
'settings.offline.stats.pending': 'Ootel muudatused',
'settings.offline.stats.failed': 'Ebaõnnestunud muudatused',
+ 'settings.offline.failed.hint':
+ 'Need muudatused ei jõudnud kunagi serverisse. Proovi uuesti või loobu neist, et säilitada serveri versioon.',
+ 'settings.offline.failed.retry': 'Proovi uuesti',
+ 'settings.offline.failed.discard': 'Loobu',
'settings.offline.stats.conflicts': 'Konfliktid',
'settings.offline.empty': 'Reise pole veel vahemällu salvestatud. Sünkroonimiseks loo internetiühendus.',
'settings.offline.loading': 'Laadimine…',
diff --git a/shared/src/i18n/fr/settings.ts b/shared/src/i18n/fr/settings.ts
index 2575d05fc..9ee8a64f9 100644
--- a/shared/src/i18n/fr/settings.ts
+++ b/shared/src/i18n/fr/settings.ts
@@ -563,6 +563,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Voyages en cache',
'settings.offline.stats.pending': 'Modifications en attente',
'settings.offline.stats.failed': 'Modifications échouées',
+ 'settings.offline.failed.hint':
+ 'Ces modifications ne sont jamais parvenues au serveur. Réessayez, ou abandonnez-les pour garder la version du serveur.',
+ 'settings.offline.failed.retry': 'Réessayer',
+ 'settings.offline.failed.discard': 'Abandonner',
'settings.offline.stats.conflicts': 'Conflits',
'settings.offline.empty': "Aucun voyage en cache pour l'instant. Connectez-vous à Internet pour synchroniser.",
'settings.offline.loading': 'Chargement…',
diff --git a/shared/src/i18n/gr/settings.ts b/shared/src/i18n/gr/settings.ts
index f8ba8a78d..51864a952 100644
--- a/shared/src/i18n/gr/settings.ts
+++ b/shared/src/i18n/gr/settings.ts
@@ -563,6 +563,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Ταξίδια σε προσωρινή μνήμη',
'settings.offline.stats.pending': 'Εκκρεμείς αλλαγές',
'settings.offline.stats.failed': 'Αποτυχημένες αλλαγές',
+ 'settings.offline.failed.hint':
+ 'Αυτές οι αλλαγές δεν έφτασαν ποτέ στον διακομιστή. Δοκιμάστε ξανά ή απορρίψτε τες για να κρατήσετε την έκδοση του διακομιστή.',
+ 'settings.offline.failed.retry': 'Δοκιμή ξανά',
+ 'settings.offline.failed.discard': 'Απόρριψη',
'settings.offline.stats.conflicts': 'Διενέξεις',
'settings.offline.empty': 'Δεν υπάρχουν ταξίδια σε προσωρινή μνήμη ακόμη. Συνδεθείτε στο διαδίκτυο για συγχρονισμό.',
'settings.offline.loading': 'Φόρτωση…',
diff --git a/shared/src/i18n/hu/settings.ts b/shared/src/i18n/hu/settings.ts
index be0daee8f..a93a60973 100644
--- a/shared/src/i18n/hu/settings.ts
+++ b/shared/src/i18n/hu/settings.ts
@@ -552,6 +552,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Gyorsítótárazott utazások',
'settings.offline.stats.pending': 'Függőben lévő módosítások',
'settings.offline.stats.failed': 'Sikertelen módosítások',
+ 'settings.offline.failed.hint':
+ 'Ezek a módosítások nem jutottak el a szerverre. Próbáld újra, vagy vesd el őket, hogy a szerveren lévő változat maradjon.',
+ 'settings.offline.failed.retry': 'Újrapróbálás',
+ 'settings.offline.failed.discard': 'Elvetés',
'settings.offline.stats.conflicts': 'Ütközések',
'settings.offline.empty': 'Még nincsenek gyorsítótárazott utazások. Csatlakozz az internethez a szinkronizáláshoz.',
'settings.offline.loading': 'Betöltés…',
diff --git a/shared/src/i18n/id/settings.ts b/shared/src/i18n/id/settings.ts
index 0bb32c73a..d3fd586b6 100644
--- a/shared/src/i18n/id/settings.ts
+++ b/shared/src/i18n/id/settings.ts
@@ -550,6 +550,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Perjalanan di cache',
'settings.offline.stats.pending': 'Perubahan tertunda',
'settings.offline.stats.failed': 'Perubahan gagal',
+ 'settings.offline.failed.hint':
+ 'Perubahan ini tidak pernah sampai ke server. Coba lagi, atau buang untuk mempertahankan versi di server.',
+ 'settings.offline.failed.retry': 'Coba lagi',
+ 'settings.offline.failed.discard': 'Buang',
'settings.offline.stats.conflicts': 'Konflik',
'settings.offline.empty': 'Belum ada perjalanan di cache. Sambungkan ke internet untuk menyinkronkan.',
'settings.offline.loading': 'Memuat…',
diff --git a/shared/src/i18n/it/settings.ts b/shared/src/i18n/it/settings.ts
index 305ae4878..30abb6b04 100644
--- a/shared/src/i18n/it/settings.ts
+++ b/shared/src/i18n/it/settings.ts
@@ -556,6 +556,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Viaggi in cache',
'settings.offline.stats.pending': 'Modifiche in sospeso',
'settings.offline.stats.failed': 'Modifiche non riuscite',
+ 'settings.offline.failed.hint':
+ 'Queste modifiche non hanno mai raggiunto il server. Riprova oppure scartale per mantenere la versione del server.',
+ 'settings.offline.failed.retry': 'Riprova',
+ 'settings.offline.failed.discard': 'Scarta',
'settings.offline.stats.conflicts': 'Conflitti',
'settings.offline.empty': 'Nessun viaggio ancora in cache. Connettiti a internet per sincronizzare.',
'settings.offline.loading': 'Caricamento…',
diff --git a/shared/src/i18n/ja/settings.ts b/shared/src/i18n/ja/settings.ts
index 58e02e479..f491e7218 100644
--- a/shared/src/i18n/ja/settings.ts
+++ b/shared/src/i18n/ja/settings.ts
@@ -522,6 +522,9 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'キャッシュ済みの旅行',
'settings.offline.stats.pending': '保留中の変更',
'settings.offline.stats.failed': '失敗した変更',
+ 'settings.offline.failed.hint': 'これらの変更はサーバーに届いていません。もう一度試すか、破棄してサーバー上のバージョンを残してください。',
+ 'settings.offline.failed.retry': '再試行',
+ 'settings.offline.failed.discard': '破棄',
'settings.offline.stats.conflicts': '競合',
'settings.offline.empty': 'キャッシュされた旅行はまだありません。インターネットに接続して同期してください。',
'settings.offline.loading': '読み込み中…',
diff --git a/shared/src/i18n/ko/settings.ts b/shared/src/i18n/ko/settings.ts
index e18b07733..38d36d0b7 100644
--- a/shared/src/i18n/ko/settings.ts
+++ b/shared/src/i18n/ko/settings.ts
@@ -539,6 +539,9 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': '캐시된 여행',
'settings.offline.stats.pending': '대기 중인 변경 사항',
'settings.offline.stats.failed': '실패한 변경 사항',
+ 'settings.offline.failed.hint': '이 변경 사항은 서버에 전달되지 않았습니다. 다시 시도하거나 삭제하여 서버의 버전을 유지하세요.',
+ 'settings.offline.failed.retry': '다시 시도',
+ 'settings.offline.failed.discard': '삭제',
'settings.offline.stats.conflicts': '충돌',
'settings.offline.empty': '아직 캐시된 여행이 없습니다. 인터넷에 연결하여 동기화하세요.',
'settings.offline.loading': '불러오는 중…',
diff --git a/shared/src/i18n/nl/settings.ts b/shared/src/i18n/nl/settings.ts
index ca0f4a90b..dd3ba55d1 100644
--- a/shared/src/i18n/nl/settings.ts
+++ b/shared/src/i18n/nl/settings.ts
@@ -555,6 +555,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Gecachte reizen',
'settings.offline.stats.pending': 'Wijzigingen in afwachting',
'settings.offline.stats.failed': 'Mislukte wijzigingen',
+ 'settings.offline.failed.hint':
+ 'Deze wijzigingen hebben de server nooit bereikt. Probeer het opnieuw of verwijder ze om de versie van de server te houden.',
+ 'settings.offline.failed.retry': 'Opnieuw proberen',
+ 'settings.offline.failed.discard': 'Verwerpen',
'settings.offline.stats.conflicts': 'Conflicten',
'settings.offline.empty': 'Nog geen reizen gecacht. Maak verbinding met internet om te synchroniseren.',
'settings.offline.loading': 'Laden…',
diff --git a/shared/src/i18n/pl/settings.ts b/shared/src/i18n/pl/settings.ts
index c591e6506..356372df5 100644
--- a/shared/src/i18n/pl/settings.ts
+++ b/shared/src/i18n/pl/settings.ts
@@ -551,6 +551,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Podróże w pamięci podręcznej',
'settings.offline.stats.pending': 'Oczekujące zmiany',
'settings.offline.stats.failed': 'Nieudane zmiany',
+ 'settings.offline.failed.hint':
+ 'Te zmiany nigdy nie dotarły do serwera. Spróbuj ponownie lub odrzuć je, aby zachować wersję z serwera.',
+ 'settings.offline.failed.retry': 'Spróbuj ponownie',
+ 'settings.offline.failed.discard': 'Odrzuć',
'settings.offline.stats.conflicts': 'Konflikty',
'settings.offline.empty': 'Brak podróży w pamięci podręcznej. Połącz się z internetem, aby zsynchronizować.',
'settings.offline.loading': 'Ładowanie…',
diff --git a/shared/src/i18n/ru/settings.ts b/shared/src/i18n/ru/settings.ts
index 7b4913719..dc9019ab8 100644
--- a/shared/src/i18n/ru/settings.ts
+++ b/shared/src/i18n/ru/settings.ts
@@ -556,6 +556,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Кэшированные поездки',
'settings.offline.stats.pending': 'Изменения в очереди',
'settings.offline.stats.failed': 'Неудавшиеся изменения',
+ 'settings.offline.failed.hint':
+ 'Эти изменения так и не попали на сервер. Повторите попытку или отмените их, чтобы оставить версию с сервера.',
+ 'settings.offline.failed.retry': 'Повторить',
+ 'settings.offline.failed.discard': 'Отменить',
'settings.offline.stats.conflicts': 'Конфликты',
'settings.offline.empty': 'Поездки ещё не кэшированы. Подключитесь к интернету для синхронизации.',
'settings.offline.loading': 'Загрузка…',
diff --git a/shared/src/i18n/sk/settings.ts b/shared/src/i18n/sk/settings.ts
index f6f9ea5ac..060fb2a6b 100644
--- a/shared/src/i18n/sk/settings.ts
+++ b/shared/src/i18n/sk/settings.ts
@@ -561,6 +561,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Cesty vo vyrovnávacej pamäti',
'settings.offline.stats.pending': 'Čakajúce zmeny',
'settings.offline.stats.failed': 'Neúspešné zmeny',
+ 'settings.offline.failed.hint':
+ 'Tieto zmeny sa nikdy nedostali na server. Skúste to znova alebo ich zahoďte a ponechajte verziu zo servera.',
+ 'settings.offline.failed.retry': 'Skúsiť znova',
+ 'settings.offline.failed.discard': 'Zahodiť',
'settings.offline.stats.conflicts': 'Konflikty',
'settings.offline.empty': 'Zatiaľ žiadne cesty vo vyrovnávacej pamäti. Pre synchronizáciu sa pripojte k internetu.',
'settings.offline.loading': 'Načítava sa…',
diff --git a/shared/src/i18n/sv/settings.ts b/shared/src/i18n/sv/settings.ts
index 4287b637e..3bed8082d 100644
--- a/shared/src/i18n/sv/settings.ts
+++ b/shared/src/i18n/sv/settings.ts
@@ -548,6 +548,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Cachelagrade resor',
'settings.offline.stats.pending': 'Väntande ändringar',
'settings.offline.stats.failed': 'Misslyckade ändringar',
+ 'settings.offline.failed.hint':
+ 'De här ändringarna nådde aldrig servern. Försök igen, eller släng dem för att behålla serverns version.',
+ 'settings.offline.failed.retry': 'Försök igen',
+ 'settings.offline.failed.discard': 'Släng',
'settings.offline.stats.conflicts': 'Konflikter',
'settings.offline.empty': 'Inga resor har cachelagrats ännu. Anslut till internet för att synkronisera.',
'settings.offline.loading': 'Laddar…',
diff --git a/shared/src/i18n/th/settings.ts b/shared/src/i18n/th/settings.ts
index 4daaa4308..1bd65378b 100644
--- a/shared/src/i18n/th/settings.ts
+++ b/shared/src/i18n/th/settings.ts
@@ -542,6 +542,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': "ทริปที่แคชไว้",
'settings.offline.stats.pending': "รอดำเนินการเปลี่ยนแปลง",
'settings.offline.stats.failed': "การเปลี่ยนแปลงล้มเหลว",
+ 'settings.offline.failed.hint':
+ 'การเปลี่ยนแปลงเหล่านี้ไม่เคยไปถึงเซิร์ฟเวอร์ ลองอีกครั้ง หรือทิ้งไปเพื่อคงเวอร์ชันบนเซิร์ฟเวอร์ไว้',
+ 'settings.offline.failed.retry': 'ลองอีกครั้ง',
+ 'settings.offline.failed.discard': 'ทิ้ง',
'settings.offline.stats.conflicts': "ข้อขัดแย้ง",
'settings.offline.empty': "ยังไม่มีแคชการเดินทาง เชื่อมต่ออินเทอร์เน็ตเพื่อซิงค์",
'settings.offline.loading': "กำลังโหลด...",
diff --git a/shared/src/i18n/tr/settings.ts b/shared/src/i18n/tr/settings.ts
index a6e1a52f5..6a3138968 100644
--- a/shared/src/i18n/tr/settings.ts
+++ b/shared/src/i18n/tr/settings.ts
@@ -551,6 +551,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Önbelleğe alınan geziler',
'settings.offline.stats.pending': 'Bekleyen değişiklikler',
'settings.offline.stats.failed': 'Başarısız değişiklikler',
+ 'settings.offline.failed.hint':
+ 'Bu değişiklikler sunucuya hiç ulaşmadı. Yeniden deneyin ya da sunucudaki sürümü korumak için onları silin.',
+ 'settings.offline.failed.retry': 'Yeniden dene',
+ 'settings.offline.failed.discard': 'Sil',
'settings.offline.stats.conflicts': 'Çakışmalar',
'settings.offline.empty': 'Henüz önbelleğe alınmış gezi yok. Senkronize etmek için internete bağlanın.',
'settings.offline.loading': 'Yükleniyor…',
diff --git a/shared/src/i18n/uk/settings.ts b/shared/src/i18n/uk/settings.ts
index 7828a19dc..6e207abce 100644
--- a/shared/src/i18n/uk/settings.ts
+++ b/shared/src/i18n/uk/settings.ts
@@ -552,6 +552,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Кешовані поїздки',
'settings.offline.stats.pending': 'Зміни в очікуванні',
'settings.offline.stats.failed': 'Невдалі зміни',
+ 'settings.offline.failed.hint':
+ 'Ці зміни так і не потрапили на сервер. Спробуйте ще раз або відкиньте їх, щоб залишити версію із сервера.',
+ 'settings.offline.failed.retry': 'Спробувати ще раз',
+ 'settings.offline.failed.discard': 'Відкинути',
'settings.offline.stats.conflicts': 'Конфлікти',
'settings.offline.empty': 'Поки що немає кешованих поїздок. Підключіться до інтернету, щоб синхронізувати.',
'settings.offline.loading': 'Завантаження…',
diff --git a/shared/src/i18n/vi/settings.ts b/shared/src/i18n/vi/settings.ts
index daeec9af8..d3df7e004 100644
--- a/shared/src/i18n/vi/settings.ts
+++ b/shared/src/i18n/vi/settings.ts
@@ -553,6 +553,10 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': 'Chuyến đi trong bộ nhớ đệm',
'settings.offline.stats.pending': 'Thay đổi đang chờ',
'settings.offline.stats.failed': 'Thay đổi không thành công',
+ 'settings.offline.failed.hint':
+ 'Những thay đổi này chưa bao giờ đến được máy chủ. Hãy thử lại, hoặc bỏ chúng để giữ phiên bản trên máy chủ.',
+ 'settings.offline.failed.retry': 'Thử lại',
+ 'settings.offline.failed.discard': 'Bỏ',
'settings.offline.stats.conflicts': 'Xung đột',
'settings.offline.empty': 'Chưa có chuyến đi nào trong bộ nhớ đệm. Kết nối internet để đồng bộ hóa.',
'settings.offline.loading': 'Đang tải…',
diff --git a/shared/src/i18n/zh-TW/settings.ts b/shared/src/i18n/zh-TW/settings.ts
index 3f084ac8c..940f2b291 100644
--- a/shared/src/i18n/zh-TW/settings.ts
+++ b/shared/src/i18n/zh-TW/settings.ts
@@ -516,6 +516,9 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': '已快取的行程',
'settings.offline.stats.pending': '待處理的變更',
'settings.offline.stats.failed': '失敗的變更',
+ 'settings.offline.failed.hint': '這些變更從未送達伺服器。請重試,或捨棄它們以保留伺服器上的版本。',
+ 'settings.offline.failed.retry': '重試',
+ 'settings.offline.failed.discard': '捨棄',
'settings.offline.stats.conflicts': '衝突',
'settings.offline.empty': '尚未快取任何行程。請連線網際網路以同步。',
'settings.offline.loading': '載入中…',
diff --git a/shared/src/i18n/zh/settings.ts b/shared/src/i18n/zh/settings.ts
index cb27facaf..93cd476cd 100644
--- a/shared/src/i18n/zh/settings.ts
+++ b/shared/src/i18n/zh/settings.ts
@@ -512,6 +512,9 @@ const settings: TranslationStrings = {
'settings.offline.stats.trips': '已缓存行程',
'settings.offline.stats.pending': '待处理更改',
'settings.offline.stats.failed': '失败的更改',
+ 'settings.offline.failed.hint': '这些更改从未到达服务器。请重试,或放弃它们以保留服务器上的版本。',
+ 'settings.offline.failed.retry': '重试',
+ 'settings.offline.failed.discard': '放弃',
'settings.offline.stats.conflicts': '冲突',
'settings.offline.empty': '尚未缓存任何行程。请连接互联网以同步。',
'settings.offline.loading': '加载中…',