From 9f531d1cd9242468f8aba5a5c501ddb07dd1248f Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 19 Sep 2026 00:57:57 +0200 Subject: [PATCH] feat(docsync): make the conflict answer real and give a conflict a way out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binding asked at setup what should happen when both sides changed, stored the answer, and nothing ever read it: every conflict was manual whatever the binding said. The planner now acts on it — provider_wins pulls, trek_wins pushes, anything else parks the row — and never in the direction the binding forbids. Conflicts themselves had no exit in either shell. The server has resolved them since the first version and the strings have been in the locales just as long, but neither the panel nor the phone sheet ever listed the rows, so the only way to answer one was the MCP tool. Both shells now list them through one shared hook with the same three answers. Also: - The desktop connect dialog shows a refused save instead of looking as though nothing happened. - An error the server phrases as a sentence no longer reaches the screen as a raw translation key. - The scope pickers depend on the stable callback rather than the hook object, so opening one no longer re-lists the provider's folders on every render above it. --- .../Files/docsync/DocSyncBinding.tsx | 11 ++- .../components/Files/docsync/DocSyncBits.tsx | 22 +++++ .../Files/docsync/DocSyncConnectModal.tsx | 17 +++- .../Files/docsync/DocSyncPanel.test.tsx | 4 +- .../components/Files/docsync/DocSyncPanel.tsx | 71 ++++++++++++-- .../Files/docsync/DocSyncScopeModal.tsx | 6 +- .../components/Files/docsync/useConflicts.ts | 69 ++++++++++++++ .../components/Files/docsync/useDocSync.ts | 18 +++- .../screens/trip/tabs/MDocSyncSheet.tsx | 92 ++++++++++++++++++- server/src/nest/doc-sync/doc-sync.helpers.ts | 43 ++++++++- server/src/nest/doc-sync/doc-sync.service.ts | 1 + .../nest/doc-sync/doc-sync.helpers.test.ts | 38 ++++++++ shared/src/i18n/ar/docsync.ts | 7 ++ shared/src/i18n/br/docsync.ts | 7 ++ shared/src/i18n/ca/docsync.ts | 7 ++ shared/src/i18n/cs/docsync.ts | 7 ++ shared/src/i18n/de/docsync.ts | 7 ++ shared/src/i18n/en/docsync.ts | 7 ++ shared/src/i18n/es/docsync.ts | 7 ++ shared/src/i18n/fr/docsync.ts | 7 ++ shared/src/i18n/gr/docsync.ts | 7 ++ shared/src/i18n/hu/docsync.ts | 7 ++ shared/src/i18n/id/docsync.ts | 7 ++ shared/src/i18n/it/docsync.ts | 7 ++ shared/src/i18n/ja/docsync.ts | 7 ++ shared/src/i18n/ko/docsync.ts | 7 ++ shared/src/i18n/nl/docsync.ts | 7 ++ shared/src/i18n/pl/docsync.ts | 7 ++ shared/src/i18n/ru/docsync.ts | 7 ++ shared/src/i18n/sv/docsync.ts | 7 ++ shared/src/i18n/tr/docsync.ts | 7 ++ shared/src/i18n/uk/docsync.ts | 7 ++ shared/src/i18n/vi/docsync.ts | 7 ++ shared/src/i18n/zh-TW/docsync.ts | 7 ++ shared/src/i18n/zh/docsync.ts | 7 ++ 35 files changed, 534 insertions(+), 19 deletions(-) create mode 100644 client/src/components/Files/docsync/useConflicts.ts 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': '复制',