feat(docsync): make the conflict answer real and give a conflict a way out

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.
This commit is contained in:
Maurice
2026-09-19 00:57:57 +02:00
parent e6a5ca1521
commit 9f531d1cd9
35 changed files with 534 additions and 19 deletions
@@ -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({
/>
</Row>
<Row label={t('docsync.conflictPolicy')} hint={t('docsync.binding.conflictHint')}>
<CustomSelect
size="sm"
value={link.conflictPolicy}
onChange={v => void sync.updateLink(link.id, { conflictPolicy: String(v) })}
options={CONFLICT_POLICIES.map(p => ({ value: p, label: t(conflictPolicyKey(p)) }))}
/>
</Row>
<Row label={t('docsync.syncEnabled')} hint={t('docsync.binding.autoHint')}>
<span className="flex justify-end">
<ToggleSwitch
@@ -83,3 +83,25 @@ export function LastRun({ at }: { at: string | null }) {
if (Number.isNaN(ms)) return null
return <span title={new Date(ms).toLocaleString(locale)}>{relativeTime(ms, language)}</span>
}
/**
* 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'}`
}
@@ -53,7 +53,7 @@ export default function DocSyncConnectModal({
}
footer={
<div className="flex items-center justify-between gap-3">
<Verdict verdict={verdict} busy={sync.busy === 'test'} />
<Verdict verdict={verdict} busy={sync.busy === 'test'} failure={sync.error} />
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
@@ -140,9 +140,12 @@ function Field({
function Verdict({
verdict,
busy,
failure,
}: {
verdict: { connected: boolean; account?: string; error?: string } | null
busy: boolean
/** A call that failed outside the connection test — saving, most of all. */
failure: string | null
}) {
const { t } = useTranslation()
if (busy) {
@@ -153,6 +156,18 @@ function Verdict({
</span>
)
}
// A save that the server refused left the dialog open with the button back to
// normal and nothing else said, which reads as nothing having happened. The
// test verdict wins the space when there is one: it is the more specific
// answer and the one the person just asked for.
if (!verdict && failure) {
return (
<span className="flex min-w-0 items-center gap-2 text-caption text-danger">
<AlertTriangle size={14} />
<span className="truncate">{t(`docsync.error.${failure}`)}</span>
</span>
)
}
if (!verdict) return <span />
if (verdict.connected) {
return (
@@ -347,7 +347,9 @@ describe('DocSyncPanel — what needs a look', () => {
expect(rows).toHaveLength(2)
expect(within(rows[0]).getByText(t('docsync.state.conflict'))).toBeInTheDocument()
expect(within(rows[0]).getByText(t('docsync.issues.conflict'))).toBeInTheDocument()
expect(within(rows[0]).getByText('2')).toBeInTheDocument()
// The conflict row carries a button rather than a bare count: it is the one
// of these a person can act on.
expect(within(rows[0]).getByRole('button', { name: /Resolve/ })).toBeInTheDocument()
expect(within(rows[1]).getByText(t('docsync.state.too_large'))).toBeInTheDocument()
expect(within(rows[1]).getByText('1')).toBeInTheDocument()
expect(panel.queryByText(t('docsync.state.pending'))).not.toBeInTheDocument()
@@ -4,6 +4,7 @@ import Modal from '../../shared/Modal'
import { useTranslation } from '../../../i18n/TranslationContext'
import { DOCUMENT_PROVIDER_ICONS } from '../../shared/DocumentProviderIcons'
import { StateBadge } from './DocSyncBits'
import { useConflicts } from './useConflicts'
import { useDocSync, type DocSyncLink, type DocSyncProvider } from './useDocSync'
import DocSyncBinding from './DocSyncBinding'
import DocSyncConnectModal from './DocSyncConnectModal'
@@ -96,7 +97,7 @@ export default function DocSyncPanel({
sync={sync}
isOwner={isOwner}
/>
{attention > 0 && <Attention counts={sync.itemCounts} />}
{attention > 0 && <Attention counts={sync.itemCounts} tripId={tripId} sync={sync} />}
</div>
) : (
<NothingBound isOwner={isOwner} />
@@ -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<string, number> }) {
function Attention({
counts,
tripId,
sync,
}: {
counts: Record<string, number>
tripId: number | string
sync: ReturnType<typeof useDocSync>
}) {
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 (
<section className="rounded-xl border border-edge bg-surface">
<h4 className="flex items-center gap-2 border-b border-edge-faint px-4 py-2.5 text-body font-medium text-content">
@@ -271,12 +283,55 @@ function Attention({ counts }: { counts: Record<string, number> }) {
</h4>
<ul className="divide-y divide-edge-faint">
{rows.map(k => (
<li key={k} className="flex items-baseline justify-between gap-3 px-4 py-2.5">
<span className="min-w-0">
<span className="block text-body text-content">{t(`docsync.state.${k}`)}</span>
<span className="mt-0.5 block text-caption text-content-muted">{t(`docsync.issues.${k}`)}</span>
</span>
<span className="shrink-0 text-body font-medium tabular-nums text-content">{counts[k]}</span>
<li key={k} className="px-4 py-2.5">
<div className="flex items-baseline justify-between gap-3">
<span className="min-w-0">
<span className="block text-body text-content">{t(`docsync.state.${k}`)}</span>
<span className="mt-0.5 block text-caption text-content-muted">{t(`docsync.issues.${k}`)}</span>
</span>
{/* 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' ? (
<button
type="button"
onClick={() => setShowConflicts(v => !v)}
className="shrink-0 rounded-lg border border-edge px-2.5 py-1 text-caption font-medium text-content transition-colors hover:bg-surface-hover"
>
{showConflicts ? t('common.close') : t('docsync.conflict.resolve', { count: counts[k] })}
</button>
) : (
<span className="shrink-0 text-body font-medium tabular-nums text-content">{counts[k]}</span>
)}
</div>
{k === 'conflict' && showConflicts && (
<ul className="mt-3 space-y-2">
{conflicts.items === null && (
<li className="flex justify-center py-2">
<Loader2 size={14} className="animate-spin text-content-muted" />
</li>
)}
{conflicts.items?.map(item => (
<li key={item.id} className="rounded-lg bg-surface-secondary px-3 py-2.5">
<p className="truncate text-body text-content" title={item.name}>{item.name}</p>
<p className="mt-0.5 text-caption text-content-muted">{t('docsync.conflict.title')}</p>
<div className="mt-2 flex flex-wrap gap-2">
{(['trek', 'provider', 'both'] as const).map(keep => (
<button
key={keep}
type="button"
disabled={conflicts.working === item.id}
onClick={() => void conflicts.resolve(item.id, keep)}
className="rounded-lg border border-edge px-2.5 py-1 text-caption text-content-secondary transition-colors hover:bg-surface-hover disabled:opacity-50"
>
{t(`docsync.conflict.${keep === 'trek' ? 'keepTrek' : keep === 'provider' ? 'keepProvider' : 'keepBoth'}`)}
</button>
))}
</div>
</li>
))}
</ul>
)}
</li>
))}
</ul>
@@ -38,6 +38,10 @@ export default function DocSyncScopeModal({
const [working, setWorking] = useState<string | null>(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()
@@ -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<typeof useDocSync>, open: boolean) {
const [items, setItems] = useState<ConflictItem[] | null>(null)
const [working, setWorking] = useState<number | null>(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 }
}
@@ -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.<code>`, 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.<value>`, 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'
}
@@ -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 ? (
<DetailView
tripId={tripId}
link={link}
providerName={sync.providers.find(p => 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<typeof useDocSync>
isOwner: boolean
@@ -299,6 +304,8 @@ function DetailView({
</p>
)}
<MConflicts tripId={tripId} sync={sync} count={sync.itemCounts.conflict ?? 0} />
<button
type="button"
onClick={() => void sync.syncNow(link.id)}
@@ -331,6 +338,18 @@ function DetailView({
</button>
</SettingRow>
<SettingRow label={t('docsync.conflictPolicy')} hint={t('docsync.binding.conflictHint')}>
<button
type="button"
onClick={() =>
void sync.updateLink(link.id, { conflictPolicy: nextConflictPolicy(link.conflictPolicy) })
}
className="rounded-full bg-[color:var(--m-ic)] px-3 py-[6px] font-geist text-[0.6875rem] font-bold text-m-ink"
>
{t(conflictPolicyKey(link.conflictPolicy))}
</button>
</SettingRow>
<button
type="button"
onClick={onUnlink}
@@ -467,6 +486,10 @@ function ScopeView({
const [working, setWorking] = useState<string | null>(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(connectionId)
@@ -475,7 +498,7 @@ function ScopeView({
setError(res.error ?? null)
})()
return () => { cancelled = true }
}, [connectionId, sync])
}, [connectionId, sync.loadScopes])
const bind = async (scope: { scopeKey: string; label: string; remoteRootId: string | null; remoteRootPath: string | null }) => {
setWorking(scope.scopeKey)
@@ -730,3 +753,70 @@ function slugFor(title: string | undefined, tripId: number | string): string {
.slice(0, 40)
return `${base || 'trek'}-${tripId}`
}
/**
* The documents that changed in both places, with the way out.
*
* The same three answers the panel offers, through the same hook — only the
* markup is a phone's.
*/
function MConflicts({
tripId,
sync,
count,
}: {
tripId: number | string
sync: ReturnType<typeof useDocSync>
count: number
}) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const conflicts = useConflicts(tripId, sync, open)
if (count === 0) return null
return (
<section className="rounded-2xl bg-[color:var(--m-ic)] px-3 py-2.5">
<button
type="button"
onClick={() => setOpen(v => !v)}
className="flex w-full items-center justify-between gap-3 text-left"
>
<span className="min-w-0">
<span className="block font-geist text-[0.75rem] font-bold text-m-ink">{t('docsync.conflict.title')}</span>
<span className="mt-0.5 block font-geist text-[0.6875rem] text-m-muted">{t('docsync.issues.conflict')}</span>
</span>
<span className="shrink-0 font-geist text-[0.6875rem] font-bold text-m-ink">
{open ? t('common.close') : t('docsync.conflict.resolve', { count })}
</span>
</button>
{open && (
<ul className="mt-2.5 space-y-2">
{conflicts.items === null && (
<li className="flex justify-center py-2">
<Loader2 size={14} className="animate-spin text-m-muted" />
</li>
)}
{conflicts.items?.map(item => (
<li key={item.id} className="rounded-2xl bg-m-card px-3 py-2.5">
<p className="truncate font-geist text-[0.75rem] text-m-ink">{item.name}</p>
<div className="mt-2 flex flex-wrap gap-2">
{(['trek', 'provider', 'both'] as const).map(keep => (
<button
key={keep}
type="button"
disabled={conflicts.working === item.id}
onClick={() => void conflicts.resolve(item.id, keep)}
className="rounded-full bg-[color:var(--m-ic)] px-3 py-[6px] font-geist text-[0.6875rem] font-bold text-m-ink disabled:opacity-60"
>
{t(`docsync.conflict.${keep === 'trek' ? 'keepTrek' : keep === 'provider' ? 'keepProvider' : 'keepBoth'}`)}
</button>
))}
</div>
</li>
))}
</ul>
)}
</section>
)
}
+38 -5
View File
@@ -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<string, RemoteDocument>();
for (const r of remote) if (!r.isDeleted) remoteById.set(r.remoteId, r);
const localById = new Map<number, LocalDocument>();
@@ -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
@@ -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) {
@@ -65,6 +65,7 @@ const plan = (over: Partial<Parameters<typeof planReconcile>[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' })],
+7
View File
@@ -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': 'نسخ',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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': 'Αντιγραφή',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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': 'コピー',
+7
View File
@@ -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': '복사',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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': 'Копировать',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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': 'Копіювати',
+7
View File
@@ -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',
+7
View File
@@ -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': '複製',
+7
View File
@@ -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': '复制',