diff --git a/client/src/components/Files/docsync/DocSyncBinding.tsx b/client/src/components/Files/docsync/DocSyncBinding.tsx index 2776e60c5..574599e51 100644 --- a/client/src/components/Files/docsync/DocSyncBinding.tsx +++ b/client/src/components/Files/docsync/DocSyncBinding.tsx @@ -8,7 +8,7 @@ import Tooltip from '../../shared/Tooltip' import { useTranslation } from '../../../i18n/TranslationContext' import { DOCUMENT_PROVIDER_ICONS } from '../../shared/DocumentProviderIcons' import DocSyncFlow, { type SyncDirection } from './DocSyncFlow' -import { Badge, LastRun, StateBadge } from './DocSyncBits' +import { Badge, CONFLICT_POLICIES, conflictPolicyKey, LastRun, StateBadge } from './DocSyncBits' import type { DocSyncLink, DocSyncProvider, useDocSync } from './useDocSync' /** @@ -160,6 +160,15 @@ export default function DocSyncBinding({ /> + + void sync.updateLink(link.id, { conflictPolicy: String(v) })} + options={CONFLICT_POLICIES.map(p => ({ value: p, label: t(conflictPolicyKey(p)) }))} + /> + + {relativeTime(ms, language)} } + +/** + * The three answers to "both sides changed", in the order they are offered. + * + * Shared because the phone offers them as one cycling button and the desktop as + * a dropdown: two lists would drift, and a binding would then say one thing on + * a laptop and another on a phone. + */ +export const CONFLICT_POLICIES = ['manual', 'trek_wins', 'provider_wins'] as const + +export type ConflictPolicy = (typeof CONFLICT_POLICIES)[number] + +/** The next answer in that ring, for the phone's single button. */ +export function nextConflictPolicy(current: string): ConflictPolicy { + const at = (CONFLICT_POLICIES as readonly string[]).indexOf(current) + return CONFLICT_POLICIES[(at + 1) % CONFLICT_POLICIES.length] +} + +/** The label key for one answer. */ +export function conflictPolicyKey(policy: string): string { + return `docsync.onConflict.${(CONFLICT_POLICIES as readonly string[]).includes(policy) ? policy : 'manual'}` +} diff --git a/client/src/components/Files/docsync/DocSyncConnectModal.tsx b/client/src/components/Files/docsync/DocSyncConnectModal.tsx index e2a8f8c96..5db4f28c1 100644 --- a/client/src/components/Files/docsync/DocSyncConnectModal.tsx +++ b/client/src/components/Files/docsync/DocSyncConnectModal.tsx @@ -53,7 +53,7 @@ export default function DocSyncConnectModal({ } footer={
- +
) : ( @@ -260,9 +261,20 @@ function StoreButton({ } /** What a person still has to decide about, named rather than counted in a chip. */ -function Attention({ counts }: { counts: Record }) { +function Attention({ + counts, + tripId, + sync, +}: { + counts: Record + tripId: number | string + sync: ReturnType +}) { const { t } = useTranslation() const rows = ATTENTION_STATES.filter(k => (counts[k] ?? 0) > 0) + const [showConflicts, setShowConflicts] = useState(false) + const conflicts = useConflicts(tripId, sync, showConflicts) + return (

@@ -271,12 +283,55 @@ function Attention({ counts }: { counts: Record }) {

    {rows.map(k => ( -
  • - - {t(`docsync.state.${k}`)} - {t(`docsync.issues.${k}`)} - - {counts[k]} +
  • +
    + + {t(`docsync.state.${k}`)} + {t(`docsync.issues.${k}`)} + + {/* A conflict is the one of these a person can act on, so it is + the one that opens. The rest are a state to read. */} + {k === 'conflict' ? ( + + ) : ( + {counts[k]} + )} +
    + + {k === 'conflict' && showConflicts && ( +
      + {conflicts.items === null && ( +
    • + +
    • + )} + {conflicts.items?.map(item => ( +
    • +

      {item.name}

      +

      {t('docsync.conflict.title')}

      +
      + {(['trek', 'provider', 'both'] as const).map(keep => ( + + ))} +
      +
    • + ))} +
    + )}
  • ))}
diff --git a/client/src/components/Files/docsync/DocSyncScopeModal.tsx b/client/src/components/Files/docsync/DocSyncScopeModal.tsx index 439e73abc..811b76f91 100644 --- a/client/src/components/Files/docsync/DocSyncScopeModal.tsx +++ b/client/src/components/Files/docsync/DocSyncScopeModal.tsx @@ -38,6 +38,10 @@ export default function DocSyncScopeModal({ const [working, setWorking] = useState(null) useEffect(() => { + // `sync.loadScopes`, not `sync`: the hook hands back a fresh object on + // every render of the panel above, so depending on it re-listed the + // provider's folders each time anything up there changed. The callback + // itself is stable. let cancelled = false void (async () => { const res = await sync.loadScopes(connection.id) @@ -46,7 +50,7 @@ export default function DocSyncScopeModal({ setError(res.error ?? null) })() return () => { cancelled = true } - }, [connection.id, sync]) + }, [connection.id, sync.loadScopes]) const shown = useMemo(() => { const needle = query.trim().toLowerCase() diff --git a/client/src/components/Files/docsync/useConflicts.ts b/client/src/components/Files/docsync/useConflicts.ts new file mode 100644 index 000000000..7e0071f15 --- /dev/null +++ b/client/src/components/Files/docsync/useConflicts.ts @@ -0,0 +1,69 @@ +import { useCallback, useEffect, useState } from 'react' +import { docsyncApi } from '../../../api/client' +import type { useDocSync } from './useDocSync' + +/** + * The documents that changed in both places, and the way out of that. + * + * The server has resolved conflicts since the first version and the strings + * have been in the locales just as long, but neither shell ever listed them: a + * binding reported "1 conflict" and the only place to act on it was the MCP + * tool. So a conflict was a dead end for anyone using the app. + * + * Shared between the panel and the phone sheet because both list the same rows + * with the same three answers, and because the duplication gate counts a second + * copy of it against the PR. + */ + +export interface ConflictItem { + id: number + /** The trip file's name, or the provider's when TREK never got a copy. */ + name: string + remoteName: string | null +} + +interface ItemRow { + id: number + file_name?: string | null + remote_name?: string | null +} + +export function useConflicts(tripId: number | string, sync: ReturnType, open: boolean) { + const [items, setItems] = useState(null) + const [working, setWorking] = useState(null) + + const reload = useCallback(async () => { + const rows = (await docsyncApi.items(tripId, 'conflict')) as ItemRow[] + setItems( + rows.map(r => ({ + id: r.id, + name: r.file_name || r.remote_name || '', + remoteName: r.remote_name ?? null, + })), + ) + }, [tripId]) + + // Only once the list is on screen: a trip with no conflicts should not pay + // for a request to find that out, and the count above it already says so. + useEffect(() => { + if (open) void reload() + }, [open, reload]) + + const resolve = useCallback( + async (itemId: number, keep: 'trek' | 'provider' | 'both') => { + setWorking(itemId) + try { + await sync.resolveConflict(itemId, keep) + // The row is gone from this list either way — it was resolved, or the + // call failed and `sync.error` says so. Re-reading is what tells the + // difference, rather than removing it here and hoping. + await reload() + } finally { + setWorking(null) + } + }, + [sync, reload], + ) + + return { items, working, resolve, reload } +} diff --git a/client/src/components/Files/docsync/useDocSync.ts b/client/src/components/Files/docsync/useDocSync.ts index da96ec874..e2f63c70b 100644 --- a/client/src/components/Files/docsync/useDocSync.ts +++ b/client/src/components/Files/docsync/useDocSync.ts @@ -330,7 +330,23 @@ export function useDocSync(tripId: number | string, enabled: boolean) { } } +/** + * The error code a failed call carries, or `unknown`. + * + * Both shells render this as `docsync.error.`, so anything that is not + * one of the codes reaches the screen as a raw translation key — and the server + * answers plenty of routes with a sentence ("Link not found") rather than a + * code. A general message is worse than a specific one and far better than + * `docsync.error.Link not found`. + */ function readError(e: unknown): string { const res = (e as { response?: { data?: { error?: string; message?: string } } })?.response - return res?.data?.error || res?.data?.message || 'unknown' + const raw = res?.data?.error || res?.data?.message || '' + // A code passes through — the sync codes and the handful of keys the routes + // answer with are all of this shape. A sentence does not: several routes + // answer with prose ("Link not found"), and both shells render this as + // `docsync.error.`, so that reached the screen as a raw translation + // key. A general message is worse than a specific one and far better than + // `docsync.error.Link not found`. + return /^[a-z0-9_.]+$/i.test(raw) ? raw : 'unknown' } diff --git a/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx b/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx index bcde38e05..23aff8dbc 100644 --- a/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx +++ b/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx @@ -15,6 +15,8 @@ import { useDocSync, type DocSyncLink, type DocSyncProvider, } from '../../../../components/Files/docsync/useDocSync' import { useConnectForm } from '../../../../components/Files/docsync/useConnectForm' +import { conflictPolicyKey, nextConflictPolicy } from '../../../../components/Files/docsync/DocSyncBits' +import { useConflicts } from '../../../../components/Files/docsync/useConflicts' import { relativeTime } from '../../../../utils/relativeTime' /** @@ -123,6 +125,7 @@ export default function MDocSyncSheet({ /> ) : view === 'detail' && link ? ( p.id === link.providerId)?.name ?? link.providerId} sync={sync} @@ -224,12 +227,14 @@ function ListView({ /** One binding: the flow, a run button, its settings. */ function DetailView({ link, + tripId, providerName, sync, isOwner, onUnlink, }: { link: DocSyncLink + tripId: number | string providerName: string sync: ReturnType isOwner: boolean @@ -299,6 +304,8 @@ function DetailView({

)} + + + + + + + + {open && ( +
    + {conflicts.items === null && ( +
  • + +
  • + )} + {conflicts.items?.map(item => ( +
  • +

    {item.name}

    +
    + {(['trek', 'provider', 'both'] as const).map(keep => ( + + ))} +
    +
  • + ))} +
+ )} +
+ ) +} diff --git a/server/src/nest/doc-sync/doc-sync.helpers.ts b/server/src/nest/doc-sync/doc-sync.helpers.ts index 5f52de904..3b7c4017b 100644 --- a/server/src/nest/doc-sync/doc-sync.helpers.ts +++ b/server/src/nest/doc-sync/doc-sync.helpers.ts @@ -168,6 +168,15 @@ export interface ReconcileInput { * Nextcloud or OpenCloud binding tripped the mass-delete guard for good. */ remoteUnchanged: boolean; + /** + * What to do when both sides moved since they last agreed. + * + * `manual` parks the row for a person. The other two are the reason this + * field exists: the binding was asked at setup, the answer was stored, and + * nothing ever read it — so every conflict was manual whatever the binding + * said, and a person who had chosen a side still had to pick it again. + */ + conflictPolicy: 'manual' | 'trek_wins' | 'provider_wins'; /** Providers without stable ids need the rename heuristic. */ stableRemoteIds: boolean; /** How many failures a row gets before it is shelved for a person to look at. */ @@ -186,7 +195,7 @@ export interface ReconcileInput { * or ping-pong forever. */ export function planReconcile(input: ReconcileInput): ReconcilePlan { - const { items, remote, local, direction, remoteTruncated, stableRemoteIds, maxAttempts } = input; + const { items, remote, local, direction, remoteTruncated, stableRemoteIds, maxAttempts, conflictPolicy } = input; const now = input.now ?? new Date().toISOString().replace('T', ' ').slice(0, 19); const actions: PlanAction[] = []; @@ -223,6 +232,29 @@ export function planReconcile(input: ReconcileInput): ReconcilePlan { actions.push(action); }; + /** + * Both sides moved. The binding's answer decides, and `manual` parks it. + * + * A side that wins takes the whole document, name and bytes together, because + * the two halves of one document coming from different sides is not a state + * anybody asked for. Direction still has the last word: a pull-only binding + * cannot push even when TREK is meant to win, so it parks instead of doing + * the opposite of what was chosen. + */ + const settle = (it: SyncItemState, r: RemoteDocument, l: LocalDocument | null): void => { + // Only the two answers that name a winner act; anything else — `manual`, a + // value from a future version, a column somebody edited by hand — parks the + // row. Overwriting one side is the destructive move, so it needs a yes. + const park = (): void => { add({ kind: 'conflict', itemId: it.id, remote: r, local: l }); }; + if (conflictPolicy === 'provider_wins' && direction !== 'push') { + return add({ kind: 'pull_update', remote: r, itemId: it.id }); + } + if (conflictPolicy === 'trek_wins' && direction !== 'pull' && l !== null) { + return add({ kind: 'push_update', local: l, itemId: it.id, remoteId: it.remoteId }); + } + return park(); + }; + const remoteById = new Map(); for (const r of remote) if (!r.isDeleted) remoteById.set(r.remoteId, r); const localById = new Map(); @@ -310,7 +342,7 @@ export function planReconcile(input: ReconcileInput): ReconcilePlan { } if (remoteChanged && !isEcho && localChanged) { - add({ kind: 'conflict', itemId: it.id, remote: r, local: l ?? null }); + settle(it, r, l ?? null); continue; } if (remoteChanged && !isEcho) { @@ -347,9 +379,10 @@ export function planReconcile(input: ReconcileInput): ReconcilePlan { continue; } if (providerRenamed && localRenamed) { - // Both sides renamed. Nothing here can pick the right one, and picking - // wrong loses a name somebody chose, so it goes to a person. - add({ kind: 'conflict', itemId: it.id, remote: r, local: l }); + // Both sides renamed. Nothing here can pick the right one on its own, + // and picking wrong loses a name somebody chose — so it follows the + // binding's answer, and goes to a person when there is none. + settle(it, r, l); continue; } // No stored name to arbitrate with (a row from before this column, or a diff --git a/server/src/nest/doc-sync/doc-sync.service.ts b/server/src/nest/doc-sync/doc-sync.service.ts index c38fdbc50..67dcd22d9 100644 --- a/server/src/nest/doc-sync/doc-sync.service.ts +++ b/server/src/nest/doc-sync/doc-sync.service.ts @@ -186,6 +186,7 @@ export class DocSyncService { remoteUnchanged: listing.data.cursorUnchanged === true, stableRemoteIds: provider.capabilities(ref).stableId, maxAttempts: ITEM_MAX_ATTEMPTS, + conflictPolicy: link.conflict_policy as 'manual' | 'trek_wins' | 'provider_wins', }); if (plan.massDeleteGuardTripped) { diff --git a/server/tests/unit/nest/doc-sync/doc-sync.helpers.test.ts b/server/tests/unit/nest/doc-sync/doc-sync.helpers.test.ts index cab8eb6d6..314127032 100644 --- a/server/tests/unit/nest/doc-sync/doc-sync.helpers.test.ts +++ b/server/tests/unit/nest/doc-sync/doc-sync.helpers.test.ts @@ -65,6 +65,7 @@ const plan = (over: Partial[0]> = {}) => direction: 'both', remoteTruncated: false, remoteUnchanged: false, + conflictPolicy: 'manual', stableRemoteIds: true, maxAttempts: 6, ...over, @@ -195,6 +196,43 @@ describe('planReconcile', () => { expect(p.actions[0]).toMatchObject({ kind: 'rename_local', name: 'new-upstream.pdf' }); }); + it('lets the provider win when the binding says so', () => { + // The answer was stored at setup and never read: every conflict was manual + // whatever the binding said, so a person who had already picked a side was + // asked to pick it again, for good. + const p = plan({ + conflictPolicy: 'provider_wins', + items: [item({ contentSha256: 'old', remoteVersion: 'v1' })], + remote: [remote({ remoteVersion: 'v2' })], + local: [local({ sha256: 'newer' })], + }); + expect(p.actions.map((a) => a.kind)).toEqual(['pull_update']); + }); + + it('lets TREK win when the binding says so', () => { + const p = plan({ + conflictPolicy: 'trek_wins', + items: [item({ contentSha256: 'old', remoteVersion: 'v1' })], + remote: [remote({ remoteVersion: 'v2' })], + local: [local({ sha256: 'newer' })], + }); + expect(p.actions.map((a) => a.kind)).toEqual(['push_update']); + }); + + it('will not resolve a conflict by doing what the direction forbids', () => { + // A pull-only binding cannot push, so "TREK wins" cannot mean "upload it". + // Parking is the honest answer; the alternative is silently doing the + // opposite of one of the two settings. + const p = plan({ + conflictPolicy: 'trek_wins', + direction: 'pull', + items: [item({ contentSha256: 'old', remoteVersion: 'v1' })], + remote: [remote({ remoteVersion: 'v2' })], + local: [local({ sha256: 'newer' })], + }); + expect(p.actions.map((a) => a.kind)).toEqual(['conflict']); + }); + it('asks a person when both sides renamed', () => { const p = plan({ items: [item({ remoteName: 'old.pdf' })], diff --git a/shared/src/i18n/ar/docsync.ts b/shared/src/i18n/ar/docsync.ts index 656fb3a62..5506d44f0 100644 --- a/shared/src/i18n/ar/docsync.ts +++ b/shared/src/i18n/ar/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'عند حذف مستند', 'docsync.deleteUnlink': 'الإبقاء على النسختين', 'docsync.deleteTrash': 'النقل إلى سلة المهملات', + 'docsync.conflictPolicy': 'عند تغيير الجانبين', + 'docsync.onConflict.manual': 'اسألني', + 'docsync.onConflict.trek_wins': 'الاحتفاظ بنسخة TREK', + 'docsync.onConflict.provider_wins': 'الاحتفاظ بنسخة المخزن', 'docsync.webhookHint': 'الصق هذا الـ URL في المزوّد لتصل التغييرات فوراً. بدونه يتحقق TREK على فترات زمنية.', // حقول نموذج الاتصال. تطابق المفاتيح عمود `label` في @@ -64,6 +68,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'محذوف في TREK', 'docsync.state.scope_drift': 'نُقل خارج المجلد', + 'docsync.conflict.resolve': "حلّ {count}", + 'docsync.conflict.title': 'تغيّرت النسختان', 'docsync.conflict.keepTrek': 'الإبقاء على نسخة TREK', 'docsync.conflict.keepProvider': 'الإبقاء على نسخة المزوّد', @@ -127,6 +133,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'متوقفة مؤقتاً', 'docsync.binding.neverRun': 'لم تُشغَّل بعد', 'docsync.binding.deleteHint': 'ما الذي يحدث للنسخة في الجهة الأخرى.', + 'docsync.binding.conflictHint': 'أي نسخة تبقى عند تحرير مستند في المكانين.', 'docsync.binding.autoHint': 'التحقق من التغييرات في الخلفية.', 'docsync.binding.webhookTitle': 'تحديثات فورية', 'docsync.binding.copy': 'نسخ', diff --git a/shared/src/i18n/br/docsync.ts b/shared/src/i18n/br/docsync.ts index 715f06c8d..6da2cd186 100644 --- a/shared/src/i18n/br/docsync.ts +++ b/shared/src/i18n/br/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Quando um documento é excluído', 'docsync.deleteUnlink': 'Manter as duas cópias', 'docsync.deleteTrash': 'Mover para a lixeira', + 'docsync.conflictPolicy': 'Quando os dois lados mudaram', + 'docsync.onConflict.manual': 'Perguntar', + 'docsync.onConflict.trek_wins': 'Manter a cópia do TREK', + 'docsync.onConflict.provider_wins': 'Manter a cópia do armazenamento', 'docsync.webhookHint': 'Cole esta URL no seu provedor para que as mudanças cheguem na hora. Sem isso, o TREK verifica em intervalos.', @@ -66,6 +70,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Excluído no TREK', 'docsync.state.scope_drift': 'Saiu da pasta', + 'docsync.conflict.resolve': "Resolver {count}", + 'docsync.conflict.title': 'As duas cópias mudaram', 'docsync.conflict.keepTrek': 'Manter a versão do TREK', 'docsync.conflict.keepProvider': 'Manter a versão do provedor', @@ -132,6 +138,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Pausado', 'docsync.binding.neverRun': 'ainda não executado', 'docsync.binding.deleteHint': 'O que acontece com a cópia do outro lado.', + 'docsync.binding.conflictHint': 'Qual cópia fica quando um documento foi editado nos dois lugares.', 'docsync.binding.autoHint': 'Procurar mudanças em segundo plano.', 'docsync.binding.webhookTitle': 'Atualizações na hora', 'docsync.binding.copy': 'Copiar', diff --git a/shared/src/i18n/ca/docsync.ts b/shared/src/i18n/ca/docsync.ts index dd86c3f03..d7381b0ba 100644 --- a/shared/src/i18n/ca/docsync.ts +++ b/shared/src/i18n/ca/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Quan s’elimina un document', 'docsync.deleteUnlink': 'Conserva les dues còpies', 'docsync.deleteTrash': 'Mou a la paperera', + 'docsync.conflictPolicy': 'Quan han canviat les dues bandes', + 'docsync.onConflict.manual': "Pregunta-m'ho", + 'docsync.onConflict.trek_wins': 'Conserva la còpia del TREK', + 'docsync.onConflict.provider_wins': 'Conserva la còpia del magatzem', 'docsync.webhookHint': 'Enganxa aquesta URL al teu proveïdor perquè els canvis arribin de seguida. Sense això, TREK ho comprova cada cert temps.', @@ -67,6 +71,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Eliminat a TREK', 'docsync.state.scope_drift': 'Ha sortit de la carpeta', + 'docsync.conflict.resolve': "Resol {count}", + 'docsync.conflict.title': 'Han canviat totes dues còpies', 'docsync.conflict.keepTrek': 'Conserva la versió de TREK', 'docsync.conflict.keepProvider': 'Conserva la versió del proveïdor', @@ -133,6 +139,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'En pausa', 'docsync.binding.neverRun': 'encara no s’ha executat', 'docsync.binding.deleteHint': 'Què passa amb la còpia de l’altra banda.', + 'docsync.binding.conflictHint': "Quina còpia es manté quan un document s'ha editat als dos llocs.", 'docsync.binding.autoHint': 'Comprova si hi ha canvis en segon pla.', 'docsync.binding.webhookTitle': 'Actualitzacions instantànies', 'docsync.binding.copy': 'Copia', diff --git a/shared/src/i18n/cs/docsync.ts b/shared/src/i18n/cs/docsync.ts index 2272f53b1..1cfeea40e 100644 --- a/shared/src/i18n/cs/docsync.ts +++ b/shared/src/i18n/cs/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Když se dokument smaže', 'docsync.deleteUnlink': 'Zachovat obě kopie', 'docsync.deleteTrash': 'Přesunout do koše', + 'docsync.conflictPolicy': 'Když se změnily obě strany', + 'docsync.onConflict.manual': 'Zeptat se', + 'docsync.onConflict.trek_wins': 'Ponechat kopii z TREKu', + 'docsync.onConflict.provider_wins': 'Ponechat kopii z úložiště', 'docsync.webhookHint': 'Vložte tuto URL k poskytovateli, aby změny přicházely okamžitě. Bez toho se TREK ptá v pravidelných intervalech.', @@ -65,6 +69,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Smazáno v TREKu', 'docsync.state.scope_drift': 'Přesunuto mimo složku', + 'docsync.conflict.resolve': "Vyřešit {count}", + 'docsync.conflict.title': 'Změnily se obě kopie', 'docsync.conflict.keepTrek': 'Ponechat verzi z TREKu', 'docsync.conflict.keepProvider': 'Ponechat verzi poskytovatele', @@ -130,6 +136,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Pozastaveno', 'docsync.binding.neverRun': 'zatím neproběhlo', 'docsync.binding.deleteHint': 'Co se stane s kopií na druhé straně.', + 'docsync.binding.conflictHint': 'Která kopie zůstane, když byl dokument upraven na obou místech.', 'docsync.binding.autoHint': 'Kontrolovat změny na pozadí.', 'docsync.binding.webhookTitle': 'Okamžité aktualizace', 'docsync.binding.copy': 'Kopírovat', diff --git a/shared/src/i18n/de/docsync.ts b/shared/src/i18n/de/docsync.ts index 5f060d15a..f4db0f602 100644 --- a/shared/src/i18n/de/docsync.ts +++ b/shared/src/i18n/de/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Wenn ein Dokument gelöscht wird', 'docsync.deleteUnlink': 'Beide Kopien behalten', 'docsync.deleteTrash': 'In den Papierkorb verschieben', + 'docsync.conflictPolicy': 'Wenn beide Seiten geändert wurden', + 'docsync.onConflict.manual': 'Mich fragen', + 'docsync.onConflict.trek_wins': 'Die TREK-Kopie behalten', + 'docsync.onConflict.provider_wins': 'Die Kopie im Speicher behalten', 'docsync.webhookHint': 'Trage diese URL bei deinem Anbieter ein, damit Änderungen sofort ankommen. Ohne sie fragt TREK in festen Abständen nach.', @@ -67,6 +71,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'In TREK gelöscht', 'docsync.state.scope_drift': 'Aus dem Ordner verschoben', + 'docsync.conflict.resolve': "{count} klären", + 'docsync.conflict.title': 'Beide Kopien wurden geändert', 'docsync.conflict.keepTrek': 'Die TREK-Version behalten', 'docsync.conflict.keepProvider': 'Die Version des Anbieters behalten', @@ -132,6 +138,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Pausiert', 'docsync.binding.neverRun': 'noch nie gelaufen', 'docsync.binding.deleteHint': 'Was mit der Kopie auf der anderen Seite geschieht.', + 'docsync.binding.conflictHint': 'Welche Kopie bleibt, wenn ein Dokument an beiden Orten bearbeitet wurde.', 'docsync.binding.autoHint': 'Im Hintergrund nach Änderungen suchen.', 'docsync.binding.webhookTitle': 'Sofortige Updates', 'docsync.binding.copy': 'Kopieren', diff --git a/shared/src/i18n/en/docsync.ts b/shared/src/i18n/en/docsync.ts index 73c919f6b..3bcfdd553 100644 --- a/shared/src/i18n/en/docsync.ts +++ b/shared/src/i18n/en/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'When a document is deleted', 'docsync.deleteUnlink': 'Keep both copies', 'docsync.deleteTrash': 'Move to recycle bin', + 'docsync.conflictPolicy': 'When both sides changed', + 'docsync.onConflict.manual': 'Ask me', + 'docsync.onConflict.trek_wins': 'Keep the TREK copy', + 'docsync.onConflict.provider_wins': "Keep the store's copy", 'docsync.webhookHint': 'Paste this URL into your provider so changes arrive immediately. Without it, TREK checks on a timer.', @@ -65,6 +69,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Deleted in TREK', 'docsync.state.scope_drift': 'Moved out of the folder', + 'docsync.conflict.resolve': "Resolve {count}", + 'docsync.conflict.title': 'Both copies changed', 'docsync.conflict.keepTrek': 'Keep the TREK version', 'docsync.conflict.keepProvider': 'Keep the provider version', @@ -130,6 +136,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Paused', 'docsync.binding.neverRun': 'not run yet', 'docsync.binding.deleteHint': 'What happens to the copy on the other side.', + 'docsync.binding.conflictHint': 'Which copy survives when a document was edited in both places.', 'docsync.binding.autoHint': 'Check for changes in the background.', 'docsync.binding.webhookTitle': 'Instant updates', 'docsync.binding.copy': 'Copy', diff --git a/shared/src/i18n/es/docsync.ts b/shared/src/i18n/es/docsync.ts index d3711761c..481159eab 100644 --- a/shared/src/i18n/es/docsync.ts +++ b/shared/src/i18n/es/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Cuando se elimina un documento', 'docsync.deleteUnlink': 'Conservar ambas copias', 'docsync.deleteTrash': 'Mover a la papelera', + 'docsync.conflictPolicy': 'Cuando ambos lados cambiaron', + 'docsync.onConflict.manual': 'Preguntarme', + 'docsync.onConflict.trek_wins': 'Conservar la copia de TREK', + 'docsync.onConflict.provider_wins': 'Conservar la copia del almacén', 'docsync.webhookHint': 'Pega esta URL en tu proveedor para que los cambios lleguen de inmediato. Sin ella, TREK comprueba cada cierto tiempo.', @@ -66,6 +70,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Eliminado en TREK', 'docsync.state.scope_drift': 'Fuera de la carpeta', + 'docsync.conflict.resolve': "Resolver {count}", + 'docsync.conflict.title': 'Las dos copias han cambiado', 'docsync.conflict.keepTrek': 'Conservar la versión de TREK', 'docsync.conflict.keepProvider': 'Conservar la versión del proveedor', @@ -133,6 +139,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'En pausa', 'docsync.binding.neverRun': 'aún sin ejecutar', 'docsync.binding.deleteHint': 'Qué pasa con la copia del otro lado.', + 'docsync.binding.conflictHint': 'Qué copia se queda cuando un documento se editó en los dos sitios.', 'docsync.binding.autoHint': 'Busca cambios en segundo plano.', 'docsync.binding.webhookTitle': 'Actualizaciones instantáneas', 'docsync.binding.copy': 'Copiar', diff --git a/shared/src/i18n/fr/docsync.ts b/shared/src/i18n/fr/docsync.ts index 5287cc23e..346731a7a 100644 --- a/shared/src/i18n/fr/docsync.ts +++ b/shared/src/i18n/fr/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'À la suppression d’un document', 'docsync.deleteUnlink': 'Garder les deux copies', 'docsync.deleteTrash': 'Mettre à la corbeille', + 'docsync.conflictPolicy': 'Quand les deux côtés ont changé', + 'docsync.onConflict.manual': 'Me demander', + 'docsync.onConflict.trek_wins': 'Garder la copie TREK', + 'docsync.onConflict.provider_wins': 'Garder la copie du stockage', 'docsync.webhookHint': 'Collez cette URL chez votre fournisseur pour que les changements arrivent immédiatement. Sans cela, TREK vérifie à intervalles réguliers.', @@ -67,6 +71,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Supprimé dans TREK', 'docsync.state.scope_drift': 'Sorti du dossier', + 'docsync.conflict.resolve': "Régler {count}", + 'docsync.conflict.title': 'Les deux copies ont changé', 'docsync.conflict.keepTrek': 'Garder la version TREK', 'docsync.conflict.keepProvider': 'Garder la version du fournisseur', @@ -134,6 +140,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'En pause', 'docsync.binding.neverRun': 'jamais exécutée', 'docsync.binding.deleteHint': 'Ce qu’il advient de la copie de l’autre côté.', + 'docsync.binding.conflictHint': 'Quelle copie reste quand un document a été modifié des deux côtés.', 'docsync.binding.autoHint': 'Chercher les changements en arrière-plan.', 'docsync.binding.webhookTitle': 'Mises à jour instantanées', 'docsync.binding.copy': 'Copier', diff --git a/shared/src/i18n/gr/docsync.ts b/shared/src/i18n/gr/docsync.ts index c656b5da4..84e169736 100644 --- a/shared/src/i18n/gr/docsync.ts +++ b/shared/src/i18n/gr/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Όταν διαγράφεται ένα έγγραφο', 'docsync.deleteUnlink': 'Διατήρηση και των δύο αντιγράφων', 'docsync.deleteTrash': 'Μετακίνηση στον κάδο ανακύκλωσης', + 'docsync.conflictPolicy': 'Όταν άλλαξαν και οι δύο πλευρές', + 'docsync.onConflict.manual': 'Να με ρωτάει', + 'docsync.onConflict.trek_wins': 'Διατήρηση του αντιγράφου TREK', + 'docsync.onConflict.provider_wins': 'Διατήρηση του αντιγράφου του χώρου', 'docsync.webhookHint': 'Επικολλήστε αυτό το URL στον πάροχό σας ώστε οι αλλαγές να φτάνουν αμέσως. Χωρίς αυτό, το TREK ελέγχει ανά τακτά διαστήματα.', @@ -69,6 +73,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Διαγράφηκε στο TREK', 'docsync.state.scope_drift': 'Μετακινήθηκε εκτός του φακέλου', + 'docsync.conflict.resolve': "Επίλυση {count}", + 'docsync.conflict.title': 'Άλλαξαν και τα δύο αντίγραφα', 'docsync.conflict.keepTrek': 'Διατήρηση της έκδοσης του TREK', 'docsync.conflict.keepProvider': 'Διατήρηση της έκδοσης του παρόχου', @@ -135,6 +141,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Σε παύση', 'docsync.binding.neverRun': 'δεν έχει εκτελεστεί ακόμη', 'docsync.binding.deleteHint': 'Τι γίνεται με το αντίγραφο στην άλλη πλευρά.', + 'docsync.binding.conflictHint': 'Ποιο αντίγραφο μένει όταν ένα έγγραφο άλλαξε και στα δύο σημεία.', 'docsync.binding.autoHint': 'Έλεγχος για αλλαγές στο παρασκήνιο.', 'docsync.binding.webhookTitle': 'Άμεσες ενημερώσεις', 'docsync.binding.copy': 'Αντιγραφή', diff --git a/shared/src/i18n/hu/docsync.ts b/shared/src/i18n/hu/docsync.ts index bfdd6cfd8..de45d9db2 100644 --- a/shared/src/i18n/hu/docsync.ts +++ b/shared/src/i18n/hu/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Ha egy dokumentumot törölnek', 'docsync.deleteUnlink': 'Mindkét példány megtartása', 'docsync.deleteTrash': 'Áthelyezés a kukába', + 'docsync.conflictPolicy': 'Ha mindkét oldal változott', + 'docsync.onConflict.manual': 'Kérdezzen rá', + 'docsync.onConflict.trek_wins': 'A TREK-példány megtartása', + 'docsync.onConflict.provider_wins': 'A tároló példányának megtartása', 'docsync.webhookHint': 'Illeszd be ezt az URL-t a szolgáltatódnál, hogy a változások azonnal megérkezzenek. Enélkül a TREK időzítve ellenőriz.', @@ -68,6 +72,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Törölve a TREK-ben', 'docsync.state.scope_drift': 'Kikerült a mappából', + 'docsync.conflict.resolve': "{count} megoldása", + 'docsync.conflict.title': 'Mindkét példány megváltozott', 'docsync.conflict.keepTrek': 'A TREK-verzió megtartása', 'docsync.conflict.keepProvider': 'A szolgáltató verziójának megtartása', @@ -134,6 +140,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Szüneteltetve', 'docsync.binding.neverRun': 'még nem futott', 'docsync.binding.deleteHint': 'Mi történjen a másik oldalon lévő példánnyal.', + 'docsync.binding.conflictHint': 'Melyik példány marad, ha a dokumentumot mindkét helyen szerkesztették.', 'docsync.binding.autoHint': 'Változások keresése a háttérben.', 'docsync.binding.webhookTitle': 'Azonnali frissítések', 'docsync.binding.copy': 'Másolás', diff --git a/shared/src/i18n/id/docsync.ts b/shared/src/i18n/id/docsync.ts index 96d0d5452..fbff663b9 100644 --- a/shared/src/i18n/id/docsync.ts +++ b/shared/src/i18n/id/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Saat sebuah dokumen dihapus', 'docsync.deleteUnlink': 'Simpan kedua salinan', 'docsync.deleteTrash': 'Pindahkan ke tempat sampah', + 'docsync.conflictPolicy': 'Saat kedua sisi berubah', + 'docsync.onConflict.manual': 'Tanya saya', + 'docsync.onConflict.trek_wins': 'Simpan salinan TREK', + 'docsync.onConflict.provider_wins': 'Simpan salinan penyimpanan', 'docsync.webhookHint': 'Tempelkan URL ini di penyediamu agar perubahan langsung tiba. Tanpa itu, TREK memeriksa secara berkala.', @@ -67,6 +71,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Dihapus di TREK', 'docsync.state.scope_drift': 'Dipindahkan keluar folder', + 'docsync.conflict.resolve': "Selesaikan {count}", + 'docsync.conflict.title': 'Kedua salinan berubah', 'docsync.conflict.keepTrek': 'Pertahankan versi TREK', 'docsync.conflict.keepProvider': 'Pertahankan versi penyedia', @@ -133,6 +139,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Dijeda', 'docsync.binding.neverRun': 'belum pernah dijalankan', 'docsync.binding.deleteHint': 'Apa yang terjadi pada salinan di sisi satunya.', + 'docsync.binding.conflictHint': 'Salinan mana yang bertahan saat dokumen diubah di kedua tempat.', 'docsync.binding.autoHint': 'Periksa perubahan di latar belakang.', 'docsync.binding.webhookTitle': 'Pembaruan seketika', 'docsync.binding.copy': 'Salin', diff --git a/shared/src/i18n/it/docsync.ts b/shared/src/i18n/it/docsync.ts index 98b0622f0..990b070e4 100644 --- a/shared/src/i18n/it/docsync.ts +++ b/shared/src/i18n/it/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Quando un documento viene eliminato', 'docsync.deleteUnlink': 'Mantieni entrambe le copie', 'docsync.deleteTrash': 'Sposta nel cestino', + 'docsync.conflictPolicy': 'Quando entrambi i lati sono cambiati', + 'docsync.onConflict.manual': 'Chiedimelo', + 'docsync.onConflict.trek_wins': 'Tieni la copia di TREK', + 'docsync.onConflict.provider_wins': "Tieni la copia dell'archivio", 'docsync.webhookHint': 'Incolla questo URL nel tuo provider per ricevere subito le modifiche. Senza, TREK controlla a intervalli regolari.', @@ -66,6 +70,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Eliminato in TREK', 'docsync.state.scope_drift': 'Spostato fuori dalla cartella', + 'docsync.conflict.resolve': "Risolvi {count}", + 'docsync.conflict.title': 'Entrambe le copie sono cambiate', 'docsync.conflict.keepTrek': 'Mantieni la versione di TREK', 'docsync.conflict.keepProvider': 'Mantieni la versione del provider', @@ -132,6 +138,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'In pausa', 'docsync.binding.neverRun': 'mai eseguita', 'docsync.binding.deleteHint': 'Che cosa succede alla copia dall’altra parte.', + 'docsync.binding.conflictHint': 'Quale copia resta quando un documento è stato modificato in entrambi i posti.', 'docsync.binding.autoHint': 'Controlla le modifiche in background.', 'docsync.binding.webhookTitle': 'Aggiornamenti immediati', 'docsync.binding.copy': 'Copia', diff --git a/shared/src/i18n/ja/docsync.ts b/shared/src/i18n/ja/docsync.ts index a5b9023d3..8539846b1 100644 --- a/shared/src/i18n/ja/docsync.ts +++ b/shared/src/i18n/ja/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'ドキュメントが削除されたとき', 'docsync.deleteUnlink': '両方のコピーを残す', 'docsync.deleteTrash': 'ゴミ箱へ移動する', + 'docsync.conflictPolicy': '両方が変更された場合', + 'docsync.onConflict.manual': '確認する', + 'docsync.onConflict.trek_wins': 'TREK のコピーを残す', + 'docsync.onConflict.provider_wins': '保存先のコピーを残す', 'docsync.webhookHint': 'この URL を連携先に貼り付けると、変更がすぐに届きます。設定しない場合、TREK は一定間隔で確認します。', @@ -65,6 +69,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'TREK で削除済み', 'docsync.state.scope_drift': 'フォルダー外へ移動', + 'docsync.conflict.resolve': "{count} 件を解決", + 'docsync.conflict.title': '両方のコピーが変更されました', 'docsync.conflict.keepTrek': 'TREK のバージョンを残す', 'docsync.conflict.keepProvider': '連携先のバージョンを残す', @@ -130,6 +136,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': '一時停止中', 'docsync.binding.neverRun': '未実行', 'docsync.binding.deleteHint': 'もう一方に残るコピーをどう扱うかです。', + 'docsync.binding.conflictHint': '両方で編集された文書のうち、どちらのコピーを残すか。', 'docsync.binding.autoHint': 'バックグラウンドで変更を確認します。', 'docsync.binding.webhookTitle': '即時更新', 'docsync.binding.copy': 'コピー', diff --git a/shared/src/i18n/ko/docsync.ts b/shared/src/i18n/ko/docsync.ts index 4c2907eb0..43967269c 100644 --- a/shared/src/i18n/ko/docsync.ts +++ b/shared/src/i18n/ko/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': '문서가 삭제될 때', 'docsync.deleteUnlink': '양쪽 사본 유지', 'docsync.deleteTrash': '휴지통으로 이동', + 'docsync.conflictPolicy': '양쪽이 모두 바뀌었을 때', + 'docsync.onConflict.manual': '물어보기', + 'docsync.onConflict.trek_wins': 'TREK 사본 유지', + 'docsync.onConflict.provider_wins': '저장소 사본 유지', 'docsync.webhookHint': '이 URL을 저장소에 붙여넣으면 변경 사항이 바로 전달됩니다. 설정하지 않으면 TREK이 주기적으로 확인합니다.', @@ -65,6 +69,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'TREK에서 삭제됨', 'docsync.state.scope_drift': '폴더 밖으로 이동됨', + 'docsync.conflict.resolve': "{count}건 해결", + 'docsync.conflict.title': '양쪽 사본이 모두 바뀌었습니다', 'docsync.conflict.keepTrek': 'TREK 버전 유지', 'docsync.conflict.keepProvider': '저장소 버전 유지', @@ -129,6 +135,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': '일시 중지됨', 'docsync.binding.neverRun': '아직 실행 안 함', 'docsync.binding.deleteHint': '반대쪽 사본을 어떻게 할지 정합니다.', + 'docsync.binding.conflictHint': '양쪽에서 편집된 문서 중 어느 사본을 남길지.', 'docsync.binding.autoHint': '백그라운드에서 변경 사항을 확인합니다.', 'docsync.binding.webhookTitle': '즉시 업데이트', 'docsync.binding.copy': '복사', diff --git a/shared/src/i18n/nl/docsync.ts b/shared/src/i18n/nl/docsync.ts index 6e8d55880..987d534bc 100644 --- a/shared/src/i18n/nl/docsync.ts +++ b/shared/src/i18n/nl/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Als een document wordt verwijderd', 'docsync.deleteUnlink': 'Beide kopieën behouden', 'docsync.deleteTrash': 'Naar de prullenbak verplaatsen', + 'docsync.conflictPolicy': 'Als beide kanten zijn gewijzigd', + 'docsync.onConflict.manual': 'Vraag het mij', + 'docsync.onConflict.trek_wins': 'De TREK-kopie behouden', + 'docsync.onConflict.provider_wins': 'De kopie in de opslag behouden', 'docsync.webhookHint': 'Plak deze URL bij je aanbieder, dan komen wijzigingen meteen binnen. Zonder die URL kijkt TREK op vaste tijden.', @@ -67,6 +71,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Verwijderd in TREK', 'docsync.state.scope_drift': 'Buiten de map verplaatst', + 'docsync.conflict.resolve': "{count} oplossen", + 'docsync.conflict.title': 'Beide kopieën zijn gewijzigd', 'docsync.conflict.keepTrek': 'De TREK-versie behouden', 'docsync.conflict.keepProvider': 'De versie van de aanbieder behouden', @@ -132,6 +138,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Gepauzeerd', 'docsync.binding.neverRun': 'nog niet uitgevoerd', 'docsync.binding.deleteHint': 'Wat er met de kopie aan de andere kant gebeurt.', + 'docsync.binding.conflictHint': 'Welke kopie blijft als een document op beide plekken is bewerkt.', 'docsync.binding.autoHint': 'Op de achtergrond op wijzigingen controleren.', 'docsync.binding.webhookTitle': 'Directe updates', 'docsync.binding.copy': 'Kopiëren', diff --git a/shared/src/i18n/pl/docsync.ts b/shared/src/i18n/pl/docsync.ts index 73d4a3e4f..ce401abd7 100644 --- a/shared/src/i18n/pl/docsync.ts +++ b/shared/src/i18n/pl/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Gdy dokument zostanie usunięty', 'docsync.deleteUnlink': 'Zachowaj obie kopie', 'docsync.deleteTrash': 'Przenieś do kosza', + 'docsync.conflictPolicy': 'Gdy zmieniły się obie strony', + 'docsync.onConflict.manual': 'Zapytaj mnie', + 'docsync.onConflict.trek_wins': 'Zachowaj kopię z TREK-a', + 'docsync.onConflict.provider_wins': 'Zachowaj kopię z magazynu', 'docsync.webhookHint': 'Wklej ten adres URL u swojego dostawcy, aby zmiany docierały natychmiast. Bez tego TREK sprawdza je co jakiś czas.', @@ -66,6 +70,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Usunięty w TREK', 'docsync.state.scope_drift': 'Przeniesiony poza folder', + 'docsync.conflict.resolve': "Rozwiąż {count}", + 'docsync.conflict.title': 'Obie kopie zostały zmienione', 'docsync.conflict.keepTrek': 'Zachowaj wersję z TREK', 'docsync.conflict.keepProvider': 'Zachowaj wersję dostawcy', @@ -132,6 +138,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Wstrzymane', 'docsync.binding.neverRun': 'jeszcze nie uruchomiono', 'docsync.binding.deleteHint': 'Co dzieje się z kopią po drugiej stronie.', + 'docsync.binding.conflictHint': 'Która kopia zostaje, gdy dokument zmieniono w obu miejscach.', 'docsync.binding.autoHint': 'Sprawdzaj zmiany w tle.', 'docsync.binding.webhookTitle': 'Natychmiastowe aktualizacje', 'docsync.binding.copy': 'Kopiuj', diff --git a/shared/src/i18n/ru/docsync.ts b/shared/src/i18n/ru/docsync.ts index f723474fe..a29ec1f6e 100644 --- a/shared/src/i18n/ru/docsync.ts +++ b/shared/src/i18n/ru/docsync.ts @@ -19,6 +19,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Когда документ удалён', 'docsync.deleteUnlink': 'Оставить обе копии', 'docsync.deleteTrash': 'Переместить в корзину', + 'docsync.conflictPolicy': 'Когда изменились обе стороны', + 'docsync.onConflict.manual': 'Спросить меня', + 'docsync.onConflict.trek_wins': 'Оставить копию TREK', + 'docsync.onConflict.provider_wins': 'Оставить копию хранилища', 'docsync.webhookHint': 'Вставьте этот URL у своего провайдера, чтобы изменения приходили сразу. Без него TREK проверяет по таймеру.', @@ -68,6 +72,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Удалено в TREK', 'docsync.state.scope_drift': 'Перемещено за пределы папки', + 'docsync.conflict.resolve': "Решить: {count}", + 'docsync.conflict.title': 'Изменились обе копии', 'docsync.conflict.keepTrek': 'Оставить версию TREK', 'docsync.conflict.keepProvider': 'Оставить версию провайдера', @@ -133,6 +139,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Приостановлено', 'docsync.binding.neverRun': 'ещё не запускалась', 'docsync.binding.deleteHint': 'Что происходит с копией на другой стороне.', + 'docsync.binding.conflictHint': 'Какая копия останется, если документ изменили в обоих местах.', 'docsync.binding.autoHint': 'Проверять изменения в фоне.', 'docsync.binding.webhookTitle': 'Мгновенные обновления', 'docsync.binding.copy': 'Копировать', diff --git a/shared/src/i18n/sv/docsync.ts b/shared/src/i18n/sv/docsync.ts index c8f1f9186..d452d5c00 100644 --- a/shared/src/i18n/sv/docsync.ts +++ b/shared/src/i18n/sv/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'När ett dokument tas bort', 'docsync.deleteUnlink': 'Behåll båda kopiorna', 'docsync.deleteTrash': 'Flytta till papperskorgen', + 'docsync.conflictPolicy': 'När båda sidor ändrats', + 'docsync.onConflict.manual': 'Fråga mig', + 'docsync.onConflict.trek_wins': 'Behåll TREK-kopian', + 'docsync.onConflict.provider_wins': 'Behåll lagrets kopia', 'docsync.webhookHint': 'Klistra in den här URL:en hos din leverantör så kommer ändringar direkt. Utan den kontrollerar TREK med jämna mellanrum.', @@ -64,6 +68,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Borttagen i TREK', 'docsync.state.scope_drift': 'Flyttad ut ur mappen', + 'docsync.conflict.resolve': "Lös {count}", + 'docsync.conflict.title': 'Båda kopiorna har ändrats', 'docsync.conflict.keepTrek': 'Behåll TREK-versionen', 'docsync.conflict.keepProvider': 'Behåll leverantörens version', @@ -129,6 +135,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Pausad', 'docsync.binding.neverRun': 'inte körd än', 'docsync.binding.deleteHint': 'Vad som händer med kopian på andra sidan.', + 'docsync.binding.conflictHint': 'Vilken kopia som blir kvar när ett dokument redigerats på båda ställena.', 'docsync.binding.autoHint': 'Leta efter ändringar i bakgrunden.', 'docsync.binding.webhookTitle': 'Direkta uppdateringar', 'docsync.binding.copy': 'Kopiera', diff --git a/shared/src/i18n/tr/docsync.ts b/shared/src/i18n/tr/docsync.ts index 0a45623ed..c57b18845 100644 --- a/shared/src/i18n/tr/docsync.ts +++ b/shared/src/i18n/tr/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Bir belge silindiğinde', 'docsync.deleteUnlink': 'İki kopyayı da tut', 'docsync.deleteTrash': 'Geri dönüşüm kutusuna taşı', + 'docsync.conflictPolicy': 'İki taraf da değiştiğinde', + 'docsync.onConflict.manual': 'Bana sor', + 'docsync.onConflict.trek_wins': 'TREK kopyasını tut', + 'docsync.onConflict.provider_wins': 'Depodaki kopyayı tut', 'docsync.webhookHint': 'Değişikliklerin hemen ulaşması için bu URL’yi sağlayıcınıza yapıştırın. Bu olmadan TREK belirli aralıklarla denetler.', @@ -65,6 +69,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'TREK’te silindi', 'docsync.state.scope_drift': 'Klasörün dışına taşındı', + 'docsync.conflict.resolve': "{count} tanesini çöz", + 'docsync.conflict.title': 'İki kopya da değişti', 'docsync.conflict.keepTrek': 'TREK sürümünü tut', 'docsync.conflict.keepProvider': 'Sağlayıcıdaki sürümü tut', @@ -131,6 +137,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Duraklatıldı', 'docsync.binding.neverRun': 'henüz çalışmadı', 'docsync.binding.deleteHint': 'Karşı taraftaki kopyaya ne olacağı.', + 'docsync.binding.conflictHint': 'Belge iki yerde de düzenlendiğinde hangi kopyanın kalacağı.', 'docsync.binding.autoHint': 'Değişiklikleri arka planda denetler.', 'docsync.binding.webhookTitle': 'Anında güncelleme', 'docsync.binding.copy': 'Kopyala', diff --git a/shared/src/i18n/uk/docsync.ts b/shared/src/i18n/uk/docsync.ts index 56f9fb357..fe24a396c 100644 --- a/shared/src/i18n/uk/docsync.ts +++ b/shared/src/i18n/uk/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Коли документ видалено', 'docsync.deleteUnlink': 'Залишити обидві копії', 'docsync.deleteTrash': 'Перемістити до кошика', + 'docsync.conflictPolicy': 'Коли змінилися обидві сторони', + 'docsync.onConflict.manual': 'Запитати мене', + 'docsync.onConflict.trek_wins': 'Залишити копію TREK', + 'docsync.onConflict.provider_wins': 'Залишити копію сховища', 'docsync.webhookHint': 'Вставте цю URL-адресу у свого постачальника, щоб зміни надходили одразу. Без цього TREK перевіряє їх за таймером.', @@ -67,6 +71,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Видалено в TREK', 'docsync.state.scope_drift': 'Переміщено за межі теки', + 'docsync.conflict.resolve': "Розв'язати {count}", + 'docsync.conflict.title': 'Змінилися обидві копії', 'docsync.conflict.keepTrek': 'Залишити версію TREK', 'docsync.conflict.keepProvider': 'Залишити версію постачальника', @@ -132,6 +138,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Призупинено', 'docsync.binding.neverRun': 'ще не запускалося', 'docsync.binding.deleteHint': 'Що станеться з копією на іншому боці.', + 'docsync.binding.conflictHint': 'Яка копія лишиться, якщо документ змінили в обох місцях.', 'docsync.binding.autoHint': 'Перевіряти зміни у фоновому режимі.', 'docsync.binding.webhookTitle': 'Миттєві оновлення', 'docsync.binding.copy': 'Копіювати', diff --git a/shared/src/i18n/vi/docsync.ts b/shared/src/i18n/vi/docsync.ts index 0094bd655..5d51ecbb1 100644 --- a/shared/src/i18n/vi/docsync.ts +++ b/shared/src/i18n/vi/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': 'Khi một tài liệu bị xóa', 'docsync.deleteUnlink': 'Giữ cả hai bản', 'docsync.deleteTrash': 'Chuyển vào thùng rác', + 'docsync.conflictPolicy': 'Khi cả hai bên đều thay đổi', + 'docsync.onConflict.manual': 'Hỏi tôi', + 'docsync.onConflict.trek_wins': 'Giữ bản của TREK', + 'docsync.onConflict.provider_wins': 'Giữ bản của kho', 'docsync.webhookHint': 'Dán URL này vào nhà cung cấp của bạn để thay đổi đến ngay lập tức. Nếu không, TREK sẽ kiểm tra theo định kỳ.', @@ -65,6 +69,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': 'Đã xóa trong TREK', 'docsync.state.scope_drift': 'Đã chuyển ra khỏi thư mục', + 'docsync.conflict.resolve': "Giải quyết {count}", + 'docsync.conflict.title': 'Cả hai bản đều thay đổi', 'docsync.conflict.keepTrek': 'Giữ bản TREK', 'docsync.conflict.keepProvider': 'Giữ bản của nhà cung cấp', @@ -130,6 +136,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': 'Đã tạm dừng', 'docsync.binding.neverRun': 'chưa chạy lần nào', 'docsync.binding.deleteHint': 'Điều sẽ xảy ra với bản sao ở phía bên kia.', + 'docsync.binding.conflictHint': 'Bản nào được giữ lại khi tài liệu bị sửa ở cả hai nơi.', 'docsync.binding.autoHint': 'Kiểm tra thay đổi ở chế độ nền.', 'docsync.binding.webhookTitle': 'Cập nhật tức thì', 'docsync.binding.copy': 'Sao chép', diff --git a/shared/src/i18n/zh-TW/docsync.ts b/shared/src/i18n/zh-TW/docsync.ts index 49a4d4897..b89efcb5a 100644 --- a/shared/src/i18n/zh-TW/docsync.ts +++ b/shared/src/i18n/zh-TW/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': '當文件被刪除時', 'docsync.deleteUnlink': '兩份都保留', 'docsync.deleteTrash': '移到回收桶', + 'docsync.conflictPolicy': '當兩邊都有變更時', + 'docsync.onConflict.manual': '詢問我', + 'docsync.onConflict.trek_wins': '保留 TREK 的副本', + 'docsync.onConflict.provider_wins': '保留儲存端的副本', 'docsync.webhookHint': '把這個 URL 貼到你的提供者,變更就會立刻送達。沒有它,TREK 只會定時檢查。', // 連線表單欄位。這些鍵對應 document_provider_fields 的 `label` 欄, @@ -63,6 +67,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': '已在 TREK 中刪除', 'docsync.state.scope_drift': '已移出資料夾', + 'docsync.conflict.resolve': "處理 {count} 個", + 'docsync.conflict.title': '兩份都有變更', 'docsync.conflict.keepTrek': '保留 TREK 版本', 'docsync.conflict.keepProvider': '保留提供者版本', @@ -125,6 +131,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': '已暫停', 'docsync.binding.neverRun': '尚未執行', 'docsync.binding.deleteHint': '另一邊那份副本會怎麼處理。', + 'docsync.binding.conflictHint': '當文件在兩邊都被修改時,保留哪一份副本。', 'docsync.binding.autoHint': '在背景檢查是否有變更。', 'docsync.binding.webhookTitle': '即時更新', 'docsync.binding.copy': '複製', diff --git a/shared/src/i18n/zh/docsync.ts b/shared/src/i18n/zh/docsync.ts index edd691b0a..d0a536e95 100644 --- a/shared/src/i18n/zh/docsync.ts +++ b/shared/src/i18n/zh/docsync.ts @@ -18,6 +18,10 @@ const docsync: TranslationStrings = { 'docsync.deletePolicy': '当文档被删除时', 'docsync.deleteUnlink': '两份副本都保留', 'docsync.deleteTrash': '移入回收站', + 'docsync.conflictPolicy': '当两边都有改动时', + 'docsync.onConflict.manual': '询问我', + 'docsync.onConflict.trek_wins': '保留 TREK 的副本', + 'docsync.onConflict.provider_wins': '保留存储端的副本', 'docsync.webhookHint': '把此 URL 粘贴到你的文档服务中,变更就会立即送达。不设置时,TREK 会定时检查。', // 连接表单的字段。这些键与 document_provider_fields 表的 `label` 列一一对应, @@ -63,6 +67,8 @@ const docsync: TranslationStrings = { 'docsync.state.local_deleted': '已在 TREK 中删除', 'docsync.state.scope_drift': '已移出该文件夹', + 'docsync.conflict.resolve': "处理 {count} 个", + 'docsync.conflict.title': '两份副本都有改动', 'docsync.conflict.keepTrek': '保留 TREK 版本', 'docsync.conflict.keepProvider': '保留文档服务的版本', @@ -124,6 +130,7 @@ const docsync: TranslationStrings = { 'docsync.binding.autoOff': '已暂停', 'docsync.binding.neverRun': '尚未运行', 'docsync.binding.deleteHint': '另一侧的副本会怎样。', + 'docsync.binding.conflictHint': '当文档在两边都被修改时,保留哪一份副本。', 'docsync.binding.autoHint': '在后台检查变更。', 'docsync.binding.webhookTitle': '即时更新', 'docsync.binding.copy': '复制',