diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c914a510f..85924b514 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -261,6 +261,7 @@ import { usePathHistoryStore } from './stores/pathHistoryStore'; import { usePositionHistoryStore } from './stores/positionHistoryStore'; import { useReticulumIdentityStore } from './stores/reticulumIdentityStore'; import { useReticulumPeerStore } from './stores/reticulumPeerStore'; +import { useRncpTransferStore } from './stores/rncpTransferStore'; import { useRrcSessionStore } from './stores/rrcSessionStore'; // Tabs capability filtering lives in appTabMappings.ts (computeTabMappings). @@ -1290,6 +1291,7 @@ function AppContent() { void rrcSessionsByHub; return useRrcSessionStore.getState().totalUnread(); }, [rrcUnreadByRoom, rrcUnreadByHub, rrcSessionsByHub]); + const remotePendingOffers = useRncpTransferStore((s) => s.pendingOffers.size); const rrcMessageFlat = useMemo(() => { const out: RrcChatMessage[] = []; for (const list of rrcMessages.values()) out.push(...list); @@ -2836,6 +2838,11 @@ function AppContent() { chatUnread={chatUnread} roomsUnread={roomsUnread} rrcUnread={rrcUnread} + remotePendingOffers={ + protocol === 'reticulum' && capabilities.hasReticulumRemotePanel + ? remotePendingOffers + : 0 + } collapsed={sidebarCollapsed} onToggle={handleSidebarToggle} /> diff --git a/src/renderer/components/ChatPanel.tsx b/src/renderer/components/ChatPanel.tsx index dd11500f4..41b3e99af 100644 --- a/src/renderer/components/ChatPanel.tsx +++ b/src/renderer/components/ChatPanel.tsx @@ -151,6 +151,7 @@ import { ChatPayloadText } from './ChatPayloadText'; import { HelpTooltip } from './HelpTooltip'; import { MessageStatusBadge } from './MessageStatusBadge'; import { ChatDmRncpControl } from './remote/ChatDmRncpControl'; +import { ChatDmRncpOfferBanner } from './remote/ChatDmRncpOfferBanner'; import { ReticulumAttachmentLine } from './ReticulumAttachmentLine'; import { ReticulumDmPathActions, @@ -3035,6 +3036,12 @@ function ChatPanel({ /> {/* Compose emoji picker — Linux only; macOS/Windows use native showEmojiPanel() */} + {protocol === 'reticulum' && + hasRncpTransfer && + isDmMode && + reticulumDmDestinationHash != null && ( + + )} { expect(screen.getByRole('tab', { name: 'RRC, 7 unread' })).toBeInTheDocument(); }); + it('shows Remote pending-offer badge when remotePendingOffers > 0', () => { + const onChange = vi.fn(); + render( + , + ); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /2 pending inbound file offers/i })).toBeInTheDocument(); + }); + + it('hides Remote badge when remotePendingOffers is 0', () => { + render( + , + ); + expect(screen.getByRole('tab', { name: 'Remote' })).toBeInTheDocument(); + expect(screen.queryByText('0')).not.toBeInTheDocument(); + }); + it('hides RRC unread badge when rrcUnread is 0', () => { const onChange = vi.fn(); render( diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index cf3164d36..f509aa838 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -19,6 +19,8 @@ interface SidebarProps { roomsUnread?: number; /** Unread RRC message count for RRC tab badge; 0 hides badge */ rrcUnread?: number; + /** Pending rncp inbound offers for Remote tab badge; 0 hides badge */ + remotePendingOffers?: number; /** Set of tab indices that are disabled (greyed out, non-clickable) */ disabledTabs?: Set; collapsed: boolean; @@ -33,6 +35,7 @@ export default function Sidebar({ chatUnread = 0, roomsUnread = 0, rrcUnread = 0, + remotePendingOffers = 0, disabledTabs, collapsed, onToggle, @@ -64,19 +67,26 @@ export default function Sidebar({ const showChatBadge = slotId === 'Chat' && chatUnread > 0; const showRoomsBadge = slotId === 'Rooms' && roomsUnread > 0; const showRrcBadge = slotId === 'RRC' && rrcUnread > 0; + const showRemoteBadge = slotId === 'Remote' && remotePendingOffers > 0; const badgeCount = showChatBadge ? chatUnread : showRoomsBadge ? roomsUnread : showRrcBadge ? rrcUnread - : 0; - const showBadge = showChatBadge || showRoomsBadge || showRrcBadge; + : showRemoteBadge + ? remotePendingOffers + : 0; + const showBadge = showChatBadge || showRoomsBadge || showRrcBadge || showRemoteBadge; const tabAriaLabel = showBadge - ? t('aria.tabWithUnread', { - label: displayLabel, - count: badgeCount > 99 ? '99+' : badgeCount, - }) + ? showRemoteBadge + ? t('reticulumRemote.transfer.pendingOffersBadgeAria', { + count: badgeCount > 99 ? 99 : badgeCount, + }) + : t('aria.tabWithUnread', { + label: displayLabel, + count: badgeCount > 99 ? '99+' : badgeCount, + }) : displayLabel; return ( @@ -108,7 +118,11 @@ export default function Sidebar({ {showBadge && ( - + {badgeCount > 99 ? '99+' : badgeCount} )} diff --git a/src/renderer/components/remote/ChatDmRncpControl.test.tsx b/src/renderer/components/remote/ChatDmRncpControl.test.tsx index d3f082316..13a4f6384 100644 --- a/src/renderer/components/remote/ChatDmRncpControl.test.tsx +++ b/src/renderer/components/remote/ChatDmRncpControl.test.tsx @@ -1,18 +1,67 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ensureRncpDestinationReachable } from '@/renderer/lib/ensureRncpDestinationReachable'; +import { sendRncpRequestEnable } from '@/renderer/lib/sendRncpRequestEnable'; import { useReticulumIdentityActivityStore } from '@/renderer/stores/reticulumIdentityActivityStore'; import { useReticulumRemoteAddressStore } from '@/renderer/stores/reticulumRemoteAddressStore'; import { useRncpTransferStore } from '@/renderer/stores/rncpTransferStore'; import { ChatDmRncpControl } from './ChatDmRncpControl'; +vi.mock('@/renderer/lib/ensureRncpDestinationReachable', () => ({ + ensureRncpDestinationReachable: vi.fn(), +})); + +vi.mock('@/renderer/lib/sendRncpRequestEnable', () => ({ + sendRncpRequestEnable: vi.fn(), +})); + +const addToast = vi.fn(); + +vi.mock('@/renderer/components/Toast', () => ({ + useToast: () => ({ addToast }), +})); + const PEER_HASH = 'a'.repeat(32); const PEER_IDENTITY = 'd'.repeat(32); +const DEST_HASH = 'c'.repeat(32); + +function seedSavedRncpAddress(): void { + useReticulumRemoteAddressStore.setState({ + addresses: new Map([ + [ + 'addr1', + { + id: 'addr1', + label: 'Alice', + service: 'rncp', + destination_hash: DEST_HASH, + lxmf_peer_hash: PEER_HASH, + created_at: 1, + updated_at: 1, + }, + ], + ]), + hydrated: true, + hydrate: () => Promise.resolve(), + }); +} describe('ChatDmRncpControl', () => { beforeEach(() => { + addToast.mockReset(); + vi.mocked(ensureRncpDestinationReachable).mockReset(); + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'reachable', hops: 1 }); + vi.mocked(sendRncpRequestEnable).mockReset(); + vi.mocked(sendRncpRequestEnable).mockResolvedValue({ ok: true }); + vi.mocked(window.electronAPI.reticulum.rncp.showOpenFileDialog).mockReset(); + vi.mocked(window.electronAPI.reticulum.rncp.showOpenFileDialog).mockResolvedValue({ + canceled: true, + path: null, + }); + vi.mocked(window.electronAPI.reticulum.rncp.send).mockReset(); useRncpTransferStore.getState().clearAll(); useReticulumRemoteAddressStore.setState({ addresses: new Map(), @@ -175,4 +224,67 @@ describe('ChatDmRncpControl', () => { expect(window.electronAPI.reticulum.rncp.accept).toHaveBeenCalledWith({ transfer_id: 't1' }); expect(useRncpTransferStore.getState().pendingOffers.size).toBe(0); }); + + it('hard-blocks send when the receive dest is peerUnreachable', async () => { + seedSavedRncpAddress(); + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'peerUnreachable' }); + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole('button', { name: 'Send file to Alice via rncp' })); + await user.click(screen.getByRole('button', { name: 'Send file' })); + + await waitFor(() => { + expect(ensureRncpDestinationReachable).toHaveBeenCalledWith({ + destinationHash: DEST_HASH, + lxmfPeerHash: PEER_HASH, + }); + }); + expect(window.electronAPI.reticulum.rncp.showOpenFileDialog).not.toHaveBeenCalled(); + expect(window.electronAPI.reticulum.rncp.send).not.toHaveBeenCalled(); + expect(addToast).toHaveBeenCalledWith( + 'No path to that destination. The peer may be offline.', + 'error', + ); + }); + + it('opens enable-request confirm when listenerLikelyOff and confirm sends the request', async () => { + seedSavedRncpAddress(); + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'listenerLikelyOff' }); + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole('button', { name: 'Send file to Alice via rncp' })); + await user.click(screen.getByRole('button', { name: 'Send file' })); + + expect(await screen.findByText('File receiving may be off')).toBeInTheDocument(); + expect(window.electronAPI.reticulum.rncp.showOpenFileDialog).not.toHaveBeenCalled(); + + await user.click(screen.getByRole('button', { name: 'Send enable request' })); + await waitFor(() => { + expect(sendRncpRequestEnable).toHaveBeenCalledWith(PEER_HASH); + }); + }); + + it('proceeds to the file picker when the receive dest is reachable', async () => { + seedSavedRncpAddress(); + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'reachable', hops: 2 }); + vi.mocked(window.electronAPI.reticulum.rncp.showOpenFileDialog).mockResolvedValue({ + canceled: false, + path: '/tmp/hello.txt', + }); + vi.mocked(window.electronAPI.reticulum.rncp.send).mockResolvedValue({ + ok: true, + transfer_id: 'xfer-1', + }); + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole('button', { name: 'Send file to Alice via rncp' })); + await user.click(screen.getByRole('button', { name: 'Send file' })); + + await waitFor(() => { + expect(window.electronAPI.reticulum.rncp.send).toHaveBeenCalledWith({ + destination_hash: DEST_HASH, + path: '/tmp/hello.txt', + }); + }); + }); }); diff --git a/src/renderer/components/remote/ChatDmRncpControl.tsx b/src/renderer/components/remote/ChatDmRncpControl.tsx index b74aa49d3..d5b73dc76 100644 --- a/src/renderer/components/remote/ChatDmRncpControl.tsx +++ b/src/renderer/components/remote/ChatDmRncpControl.tsx @@ -2,6 +2,7 @@ import { Upload } from 'lucide-react-motion'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ConfirmModal } from '@/renderer/components/ConfirmModal'; import { RemotePathCapabilityChip } from '@/renderer/components/remote/RemotePathCapabilityChip'; import { useToast } from '@/renderer/components/Toast'; import { useRemotePathCapability } from '@/renderer/hooks/useRemotePathCapability'; @@ -9,6 +10,7 @@ import { findLatestRncpReceiveDestShareInDmCandidates, type RncpDmShareCandidate, } from '@/renderer/lib/applyRncpReceiveDestShareFromChatHistory'; +import { ensureRncpDestinationReachable } from '@/renderer/lib/ensureRncpDestinationReachable'; import { errLikeToLogString } from '@/renderer/lib/errLikeToLogString'; import { parseReticulumDestinationInput } from '@/renderer/lib/reticulum/reticulumDestinationInput'; import { rncpOfferMatchesLxmfPeer } from '@/renderer/lib/rncpOfferPeerMatch'; @@ -84,6 +86,7 @@ export function ChatDmRncpControl({ const [destinationInput, setDestinationInput] = useState(savedAddress?.destination_hash ?? ''); const [rememberAddress, setRememberAddress] = useState(false); const [sending, setSending] = useState(false); + const [enableRequestConfirmOpen, setEnableRequestConfirmOpen] = useState(false); const [localTransferIds, setLocalTransferIds] = useState([]); const chatShareAppliedRef = useRef(null); const notifiedTerminalRef = useRef(new Set()); @@ -191,10 +194,22 @@ export function ChatDmRncpControl({ addToast(t('reticulumRemote.errors.invalidAddress'), 'error'); return; } - const picked = await window.electronAPI.reticulum.rncp.showOpenFileDialog(); - if (picked.canceled || !picked.path) return; setSending(true); try { + const reach = await ensureRncpDestinationReachable({ + destinationHash: parsedHash, + lxmfPeerHash, + }); + if (reach.status === 'peerUnreachable') { + addToast(t('reticulumRemote.transfer.peerUnreachable'), 'error'); + return; + } + if (reach.status === 'listenerLikelyOff') { + setEnableRequestConfirmOpen(true); + return; + } + const picked = await window.electronAPI.reticulum.rncp.showOpenFileDialog(); + if (picked.canceled || !picked.path) return; const res = await window.electronAPI.reticulum.rncp.send({ destination_hash: parsedHash, path: picked.path, @@ -294,6 +309,11 @@ export function ChatDmRncpControl({ } }, [addToast, dmShareCandidates, lxmfPeerHash, t]); + const handleConfirmEnableRequest = useCallback(() => { + setEnableRequestConfirmOpen(false); + void handleRequestEnable(); + }, [handleRequestEnable]); + const handleUseFromChat = useCallback(() => { const fromDm = findLatestRncpReceiveDestShareInDmCandidates(dmShareCandidates); if (fromDm) { @@ -501,10 +521,13 @@ export function ChatDmRncpControl({ type="button" disabled={!parsedHash || sending} aria-label={t('reticulumRemote.transfer.sendAria')} + aria-busy={sending} onClick={() => void handleSend()} className="w-full rounded bg-blue-700/80 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-600 disabled:opacity-50" > - {sending ? t('chatPanel.rncp.sending') : t('chatPanel.rncp.chooseAndSend')} + {sending + ? t('reticulumRemote.transfer.checkingReachability') + : t('chatPanel.rncp.chooseAndSend')} {!savedAddress && ( + + + ); + })} + + + ); +} diff --git a/src/renderer/components/remote/RemoteTransferSection.test.tsx b/src/renderer/components/remote/RemoteTransferSection.test.tsx index 858e046c3..2ba7e731b 100644 --- a/src/renderer/components/remote/RemoteTransferSection.test.tsx +++ b/src/renderer/components/remote/RemoteTransferSection.test.tsx @@ -4,7 +4,9 @@ import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { RemoteTransferSection } from '@/renderer/components/remote/RemoteTransferSection'; +import { ensureRncpDestinationReachable } from '@/renderer/lib/ensureRncpDestinationReachable'; import { DEFAULT_REMOTE_SETTINGS } from '@/renderer/lib/remoteSettingsStorage'; +import { sendRncpRequestEnable } from '@/renderer/lib/sendRncpRequestEnable'; import { useReticulumRemoteAddressStore } from '@/renderer/stores/reticulumRemoteAddressStore'; import { useRncpTransferStore } from '@/renderer/stores/rncpTransferStore'; @@ -14,12 +16,54 @@ vi.mock('@/renderer/components/Toast', () => ({ useToast: () => ({ addToast }), })); +vi.mock('@/renderer/lib/ensureRncpDestinationReachable', () => ({ + ensureRncpDestinationReachable: vi.fn(), + isRncpHexHash: (value: string) => /^[0-9a-f]{32}$/.test(value), +})); + +vi.mock('@/renderer/lib/sendRncpRequestEnable', () => ({ + sendRncpRequestEnable: vi.fn(), +})); + +vi.mock('@/renderer/hooks/useRemotePathCapability', () => ({ + useRemotePathCapability: () => ({ + capability: { + destination_hash: 'c'.repeat(32), + speed: 'high', + via_atoms: ['tcp'], + transfer_allowed: true, + shell_allowed: true, + }, + loading: false, + }), +})); + +const DEST_HASH = 'c'.repeat(32); +const LXMF_HASH = 'e'.repeat(32); + describe('RemoteTransferSection', () => { beforeEach(() => { addToast.mockReset(); + vi.mocked(ensureRncpDestinationReachable).mockReset(); + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'reachable', hops: 1 }); + vi.mocked(sendRncpRequestEnable).mockReset(); + vi.mocked(sendRncpRequestEnable).mockResolvedValue({ ok: true }); useRncpTransferStore.getState().clearAll(); useReticulumRemoteAddressStore.setState({ - addresses: new Map(), + addresses: new Map([ + [ + 'addr1', + { + id: 'addr1', + label: 'Peer', + service: 'rncp', + destination_hash: DEST_HASH, + lxmf_peer_hash: LXMF_HASH, + created_at: 1, + updated_at: 1, + }, + ], + ]), hydrated: true, hydrate: () => Promise.resolve(), }); @@ -32,6 +76,16 @@ describe('RemoteTransferSection', () => { vi.mocked(window.electronAPI.reticulum.rncp.accept).mockResolvedValue({ ok: true }); vi.mocked(window.electronAPI.reticulum.rncp.reject).mockReset(); vi.mocked(window.electronAPI.reticulum.rncp.reject).mockResolvedValue({ ok: true }); + vi.mocked(window.electronAPI.reticulum.rncp.showOpenFileDialog).mockReset(); + vi.mocked(window.electronAPI.reticulum.rncp.showOpenFileDialog).mockResolvedValue({ + canceled: false, + path: '/tmp/notes.txt', + }); + vi.mocked(window.electronAPI.reticulum.rncp.send).mockReset(); + vi.mocked(window.electronAPI.reticulum.rncp.send).mockResolvedValue({ + ok: true, + transfer_id: 'xfer-remote-1', + }); }); it('smoke-renders transfer controls and loads identity when sidecar is running', async () => { @@ -94,4 +148,238 @@ describe('RemoteTransferSection', () => { expect(window.electronAPI.reticulum.remote.getIdentity).not.toHaveBeenCalled(); }); }); + + async function prepareSendForm(user: ReturnType): Promise { + render(); + await user.type(screen.getByLabelText('rncp destination hash'), DEST_HASH); + await user.click(screen.getByRole('button', { name: 'Choose a local file to send' })); + await waitFor(() => { + expect(screen.getByText('/tmp/notes.txt')).toBeInTheDocument(); + }); + } + + async function prepareFetchForm(user: ReturnType): Promise { + render(); + await user.click(screen.getByRole('button', { name: 'Switch to fetch mode' })); + await user.type(screen.getByLabelText('rncp destination hash'), DEST_HASH); + await user.type(screen.getByLabelText('Remote file path to fetch'), '/remote/file.bin'); + } + + it('hard-blocks send when peerUnreachable', async () => { + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'peerUnreachable' }); + const user = userEvent.setup(); + await prepareSendForm(user); + await user.click(screen.getByRole('button', { name: 'Send file' })); + + await waitFor(() => { + expect(ensureRncpDestinationReachable).toHaveBeenCalledWith({ + destinationHash: DEST_HASH, + lxmfPeerHash: LXMF_HASH, + }); + }); + expect(window.electronAPI.reticulum.rncp.send).not.toHaveBeenCalled(); + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + expect(addToast).toHaveBeenCalledWith( + 'No path to that destination. The peer may be offline.', + 'error', + ); + }); + + it('hard-blocks fetch when peerUnreachable', async () => { + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'peerUnreachable' }); + vi.mocked(window.electronAPI.reticulum.rncp.fetch).mockReset(); + const user = userEvent.setup(); + await prepareFetchForm(user); + await user.click(screen.getByRole('button', { name: 'Fetch file' })); + + await waitFor(() => { + expect(ensureRncpDestinationReachable).toHaveBeenCalledWith({ + destinationHash: DEST_HASH, + lxmfPeerHash: LXMF_HASH, + }); + }); + expect(window.electronAPI.reticulum.rncp.fetch).not.toHaveBeenCalled(); + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + }); + + it('hard-blocks retry when unreachable without enable-request or IPC', async () => { + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'peerUnreachable' }); + useRncpTransferStore.getState().startTransfer({ + transfer_id: 'failed-1', + kind: 'send', + destination_hash: DEST_HASH, + file_name: 'notes.txt', + retryArgs: { path: '/tmp/notes.txt' }, + }); + useRncpTransferStore.getState().applyFailed({ + transfer_id: 'failed-1', + file_name: 'notes.txt', + error: 'timeout', + }); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Retry transfer of notes.txt' })); + + await waitFor(() => { + expect(ensureRncpDestinationReachable).toHaveBeenCalledWith({ + destinationHash: DEST_HASH, + lxmfPeerHash: LXMF_HASH, + }); + }); + expect(window.electronAPI.reticulum.rncp.send).not.toHaveBeenCalled(); + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + expect(useRncpTransferStore.getState().transfers.get('failed-1')?.retryCount).toBe(0); + }); + + it('opens enable-request confirm on listenerLikelyOff and confirm sends the request', async () => { + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'listenerLikelyOff' }); + const user = userEvent.setup(); + await prepareSendForm(user); + await user.click(screen.getByRole('button', { name: 'Send file' })); + + expect(await screen.findByText('File receiving may be off')).toBeInTheDocument(); + expect(window.electronAPI.reticulum.rncp.send).not.toHaveBeenCalled(); + + await user.click(screen.getByRole('button', { name: 'Send enable request' })); + await waitFor(() => { + expect(sendRncpRequestEnable).toHaveBeenCalledWith(LXMF_HASH); + }); + }); + + it('retry on listenerLikelyOff does not open enable-request or call IPC', async () => { + vi.mocked(ensureRncpDestinationReachable).mockResolvedValue({ status: 'listenerLikelyOff' }); + useRncpTransferStore.getState().startTransfer({ + transfer_id: 'failed-2', + kind: 'send', + destination_hash: DEST_HASH, + file_name: 'notes.txt', + retryArgs: { path: '/tmp/notes.txt' }, + }); + useRncpTransferStore.getState().applyFailed({ + transfer_id: 'failed-2', + file_name: 'notes.txt', + error: 'timeout', + }); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Retry transfer of notes.txt' })); + + await waitFor(() => { + expect(ensureRncpDestinationReachable).toHaveBeenCalled(); + }); + expect(screen.queryByText('File receiving may be off')).not.toBeInTheDocument(); + expect(window.electronAPI.reticulum.rncp.send).not.toHaveBeenCalled(); + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + expect(useRncpTransferStore.getState().transfers.get('failed-2')?.retryCount).toBe(0); + }); + + it('sends when the receive dest is reachable', async () => { + const user = userEvent.setup(); + await prepareSendForm(user); + await user.click(screen.getByRole('button', { name: 'Send file' })); + + await waitFor(() => { + expect(window.electronAPI.reticulum.rncp.send).toHaveBeenCalledWith({ + destination_hash: DEST_HASH, + path: '/tmp/notes.txt', + }); + }); + }); + + it('rejects request-enable when saved LXMF hash is missing', async () => { + useReticulumRemoteAddressStore.setState({ + addresses: new Map([ + [ + 'addr1', + { + id: 'addr1', + label: 'Peer', + service: 'rncp', + destination_hash: DEST_HASH, + lxmf_peer_hash: null, + created_at: 1, + updated_at: 1, + }, + ], + ]), + hydrated: true, + hydrate: () => Promise.resolve(), + }); + const user = userEvent.setup(); + render(); + await user.type(screen.getByLabelText('rncp destination hash'), DEST_HASH); + await user.click( + screen.getByRole('button', { name: 'Request that this peer enable file receiving' }), + ); + + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + expect(addToast).toHaveBeenCalledWith( + 'Enter a valid 32-character hex destination hash.', + 'error', + ); + }); + + it('rejects request-enable when saved LXMF hash equals the rncp dest', async () => { + useReticulumRemoteAddressStore.setState({ + addresses: new Map([ + [ + 'addr1', + { + id: 'addr1', + label: 'Peer', + service: 'rncp', + destination_hash: DEST_HASH, + lxmf_peer_hash: DEST_HASH, + created_at: 1, + updated_at: 1, + }, + ], + ]), + hydrated: true, + hydrate: () => Promise.resolve(), + }); + const user = userEvent.setup(); + render(); + await user.type(screen.getByLabelText('rncp destination hash'), DEST_HASH); + await user.click( + screen.getByRole('button', { name: 'Request that this peer enable file receiving' }), + ); + + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + expect(addToast).toHaveBeenCalledWith( + 'Enter a valid 32-character hex destination hash.', + 'error', + ); + }); + + it('rejects request-enable when saved LXMF hash is invalid', async () => { + useReticulumRemoteAddressStore.setState({ + addresses: new Map([ + [ + 'addr1', + { + id: 'addr1', + label: 'Peer', + service: 'rncp', + destination_hash: DEST_HASH, + lxmf_peer_hash: 'not-a-hash', + created_at: 1, + updated_at: 1, + }, + ], + ]), + hydrated: true, + hydrate: () => Promise.resolve(), + }); + const user = userEvent.setup(); + render(); + await user.type(screen.getByLabelText('rncp destination hash'), DEST_HASH); + await user.click( + screen.getByRole('button', { name: 'Request that this peer enable file receiving' }), + ); + + expect(sendRncpRequestEnable).not.toHaveBeenCalled(); + }); }); diff --git a/src/renderer/components/remote/RemoteTransferSection.tsx b/src/renderer/components/remote/RemoteTransferSection.tsx index be598b368..aca29b31a 100644 --- a/src/renderer/components/remote/RemoteTransferSection.tsx +++ b/src/renderer/components/remote/RemoteTransferSection.tsx @@ -1,9 +1,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ConfirmModal } from '@/renderer/components/ConfirmModal'; import { RemotePathCapabilityChip } from '@/renderer/components/remote/RemotePathCapabilityChip'; import { useToast } from '@/renderer/components/Toast'; import { useRemotePathCapability } from '@/renderer/hooks/useRemotePathCapability'; +import { + ensureRncpDestinationReachable, + isRncpHexHash, +} from '@/renderer/lib/ensureRncpDestinationReachable'; import { errLikeToLogString } from '@/renderer/lib/errLikeToLogString'; import i18n from '@/renderer/lib/i18n'; import { pushRncpListenerPolicy } from '@/renderer/lib/pushRncpListenerPolicy'; @@ -73,6 +78,8 @@ export function RemoteTransferSection({ const [remotePath, setRemotePath] = useState(''); const [pickedFile, setPickedFile] = useState(null); const [pickedSaveDir, setPickedSaveDir] = useState(null); + const [enableRequestConfirmOpen, setEnableRequestConfirmOpen] = useState(false); + const [transferBusy, setTransferBusy] = useState(false); const [identity, setIdentity] = useState<{ identity_hash: string | null; rncp_receive_hash: string | null; @@ -82,6 +89,33 @@ export function RemoteTransferSection({ const { capability, loading: capabilityLoading } = useRemotePathCapability(parsedHash); const transferAllowed = capability?.transfer_allowed ?? false; + const resolveLxmfPeerHash = useCallback((destinationHash: string): string | null => { + const dest = destinationHash.trim().toLowerCase(); + const savedForDest = useReticulumRemoteAddressStore.getState().findByDestination(dest, 'rncp'); + const peer = savedForDest?.lxmf_peer_hash?.trim().toLowerCase() ?? ''; + // Saved lxmf must be a distinct delivery hash — never the rncp.receive dest itself. + if (!isRncpHexHash(peer) || peer === dest) return null; + return peer; + }, []); + + /** Returns true when the transfer may proceed. */ + const assertReachableForTransfer = useCallback( + async (destinationHash: string, opts: { promptEnable: boolean }): Promise => { + const reach = await ensureRncpDestinationReachable({ + destinationHash, + lxmfPeerHash: resolveLxmfPeerHash(destinationHash), + }); + if (reach.status === 'reachable') return true; + if (reach.status === 'listenerLikelyOff' && opts.promptEnable) { + setEnableRequestConfirmOpen(true); + return false; + } + addToast(t('reticulumRemote.transfer.peerUnreachable'), 'error'); + return false; + }, + [addToast, resolveLxmfPeerHash, t], + ); + useEffect(() => { if (!sidecarRunning) { // eslint-disable-next-line react-hooks/set-state-in-effect -- clear stale identity when the sidecar stops @@ -114,7 +148,9 @@ export function RemoteTransferSection({ const handleSend = useCallback(async () => { if (!parsedHash || !pickedFile) return; + setTransferBusy(true); try { + if (!(await assertReachableForTransfer(parsedHash, { promptEnable: true }))) return; const res = await window.electronAPI.reticulum.rncp.send({ destination_hash: parsedHash, path: pickedFile, @@ -137,12 +173,16 @@ export function RemoteTransferSection({ } catch (e) { console.debug('[RemoteTransferSection] send ' + errLikeToLogString(e)); addToast(t('reticulumRemote.transfer.sendFailed', { error: errLikeToLogString(e) }), 'error'); + } finally { + setTransferBusy(false); } - }, [addToast, parsedHash, pickedFile, startTransfer, t]); + }, [addToast, assertReachableForTransfer, parsedHash, pickedFile, startTransfer, t]); const handleFetch = useCallback(async () => { if (!parsedHash || !remotePath.trim()) return; + setTransferBusy(true); try { + if (!(await assertReachableForTransfer(parsedHash, { promptEnable: true }))) return; const res = await window.electronAPI.reticulum.rncp.fetch({ destination_hash: parsedHash, remote_path: remotePath.trim(), @@ -171,8 +211,18 @@ export function RemoteTransferSection({ t('reticulumRemote.transfer.fetchFailed', { error: errLikeToLogString(e) }), 'error', ); + } finally { + setTransferBusy(false); } - }, [addToast, parsedHash, pickedSaveDir, remotePath, startTransfer, t]); + }, [ + addToast, + assertReachableForTransfer, + parsedHash, + pickedSaveDir, + remotePath, + startTransfer, + t, + ]); const handleCancel = useCallback(async (transferId: string) => { try { @@ -183,21 +233,31 @@ export function RemoteTransferSection({ }, []); const maxRetry = settings.maxRetryAttempts ?? DEFAULT_RNCP_MAX_RETRY_ATTEMPTS; + /** Blocks concurrent manual/auto retry of the same transfer_id. */ + const retryInFlightRef = useRef(new Set()); const handleRetry = useCallback( async (transferId: string) => { + if (retryInFlightRef.current.has(transferId)) return; const transfer = transfers.get(transferId); if (!transfer?.retryArgs) return; - // Sidecar returns a new transfer_id on each resubmit — seed the next - // record with the incremented count so auto-retry still honors maxRetry. - const nextRetryCount = incrementRetry(transferId); + retryInFlightRef.current.add(transferId); try { + // Do not open the enable-request modal from auto/manual retry loops. + // Leave retryCount / auto-retry markers alone when the dest is unreachable. + if ( + !(await assertReachableForTransfer(transfer.destination_hash, { promptEnable: false })) + ) { + return; + } if ('path' in transfer.retryArgs) { const res = await window.electronAPI.reticulum.rncp.send({ destination_hash: transfer.destination_hash, path: transfer.retryArgs.path, }); if (res.ok && res.transfer_id) { + // Sidecar returns a new transfer_id — seed the next record after accept. + const nextRetryCount = incrementRetry(transferId); startTransfer({ transfer_id: res.transfer_id, kind: 'send', @@ -214,6 +274,7 @@ export function RemoteTransferSection({ save_path: transfer.retryArgs.save_path, }); if (res.ok && res.transfer_id) { + const nextRetryCount = incrementRetry(transferId); startTransfer({ transfer_id: res.transfer_id, kind: 'fetch', @@ -226,12 +287,16 @@ export function RemoteTransferSection({ } } catch (e) { console.warn('[RemoteTransferSection] retry ' + errLikeToLogString(e)); + } finally { + retryInFlightRef.current.delete(transferId); } }, - [incrementRetry, startTransfer, transfers], + [assertReachableForTransfer, incrementRetry, startTransfer, transfers], ); // Auto-retry each failed transfer once per failure, up to the configured cap. + // Mark before await so handleRetry identity churn cannot re-enter while a probe runs; + // handleRetry only bumps retryCount after the sidecar accepts the resubmit. const autoRetriedRef = useRef(new Set()); useEffect(() => { if (!settings.autoRetryTransfer) return; @@ -239,6 +304,7 @@ export function RemoteTransferSection({ if (transfer.status !== 'failed' || !transfer.retryArgs) continue; if (transfer.retryCount >= maxRetry) continue; if (autoRetriedRef.current.has(transfer.transfer_id)) continue; + if (retryInFlightRef.current.has(transfer.transfer_id)) continue; autoRetriedRef.current.add(transfer.transfer_id); void handleRetry(transfer.transfer_id); } @@ -315,20 +381,36 @@ export function RemoteTransferSection({ ); const handleRequestEnable = useCallback(async () => { - if (!parsedHash) { + if (!parsedHash || !isRncpHexHash(parsedHash)) { addToast(t('reticulumRemote.errors.invalidAddress'), 'error'); return; } - // Request enable must target the peer's LXMF delivery hash. Prefer the saved - // lxmf_peer_hash when the destination field holds an rncp.receive hash. + // Prefer a saved LXMF delivery hash for this rncp dest. Reject missing/invalid + // or dest-duplicate saved values — never fall back to the rncp.receive hash. const savedForDest = useReticulumRemoteAddressStore .getState() .findByDestination(parsedHash, 'rncp'); - const peerLxmfHash = savedForDest?.lxmf_peer_hash?.trim() || parsedHash; + let peerLxmfHash: string; + if (savedForDest) { + const peer = savedForDest.lxmf_peer_hash?.trim().toLowerCase() ?? ''; + if (!isRncpHexHash(peer) || peer === parsedHash) { + addToast(t('reticulumRemote.errors.invalidAddress'), 'error'); + return; + } + peerLxmfHash = peer; + } else { + // No saved address: treat the destination field as a direct LXMF delivery hash. + peerLxmfHash = parsedHash; + } const res = await sendRncpRequestEnable(peerLxmfHash); toastRncpRequestEnableResult(res, addToast, t); }, [addToast, parsedHash, t]); + const handleConfirmEnableRequest = useCallback(() => { + setEnableRequestConfirmOpen(false); + void handleRequestEnable(); + }, [handleRequestEnable]); + const handleCopyInstructions = useCallback(() => { const text = buildRncpRequestEnableMessageBody( i18n.t('reticulumRemote.enableRequest.lxmfBody'), @@ -523,12 +605,17 @@ export function RemoteTransferSection({ ) : ( @@ -556,12 +643,21 @@ export function RemoteTransferSection({ )} @@ -632,6 +728,17 @@ export function RemoteTransferSection({ )) )} + {enableRequestConfirmOpen && ( + { + setEnableRequestConfirmOpen(false); + }} + /> + )} ); } diff --git a/src/renderer/lib/ensureRncpDestinationReachable.test.ts b/src/renderer/lib/ensureRncpDestinationReachable.test.ts new file mode 100644 index 000000000..4c41071b1 --- /dev/null +++ b/src/renderer/lib/ensureRncpDestinationReachable.test.ts @@ -0,0 +1,70 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@/renderer/lib/reticulum/reticulumSidecarReads', () => ({ + probeReticulumPeer: vi.fn(), +})); + +import { probeReticulumPeer } from '@/renderer/lib/reticulum/reticulumSidecarReads'; + +import { ensureRncpDestinationReachable } from './ensureRncpDestinationReachable'; + +const DEST = 'a'.repeat(32); +const LXMF = 'b'.repeat(32); + +describe('ensureRncpDestinationReachable', () => { + beforeEach(() => { + vi.mocked(probeReticulumPeer).mockReset(); + }); + + it('returns reachable when the receive dest probe succeeds', async () => { + vi.mocked(probeReticulumPeer).mockResolvedValueOnce({ ok: true, hops: 2 }); + await expect( + ensureRncpDestinationReachable({ destinationHash: DEST, lxmfPeerHash: LXMF }), + ).resolves.toEqual({ status: 'reachable', hops: 2 }); + expect(probeReticulumPeer).toHaveBeenCalledTimes(1); + expect(probeReticulumPeer).toHaveBeenCalledWith(DEST); + }); + + it('returns listenerLikelyOff when dest fails but LXMF probe succeeds', async () => { + vi.mocked(probeReticulumPeer) + .mockResolvedValueOnce({ ok: false, error: 'timeout' }) + .mockResolvedValueOnce({ ok: true, hops: 1 }); + await expect( + ensureRncpDestinationReachable({ destinationHash: DEST, lxmfPeerHash: LXMF }), + ).resolves.toEqual({ status: 'listenerLikelyOff' }); + expect(probeReticulumPeer).toHaveBeenCalledWith(DEST); + expect(probeReticulumPeer).toHaveBeenCalledWith(LXMF); + }); + + it('returns peerUnreachable when both probes fail', async () => { + vi.mocked(probeReticulumPeer) + .mockResolvedValueOnce({ ok: false }) + .mockResolvedValueOnce({ ok: false }); + await expect( + ensureRncpDestinationReachable({ destinationHash: DEST, lxmfPeerHash: LXMF }), + ).resolves.toEqual({ status: 'peerUnreachable' }); + }); + + it('returns peerUnreachable when dest fails and no LXMF hash is provided', async () => { + vi.mocked(probeReticulumPeer).mockResolvedValueOnce({ ok: false }); + await expect(ensureRncpDestinationReachable({ destinationHash: DEST })).resolves.toEqual({ + status: 'peerUnreachable', + }); + expect(probeReticulumPeer).toHaveBeenCalledTimes(1); + }); + + it('returns peerUnreachable when LXMF hash equals the receive dest', async () => { + vi.mocked(probeReticulumPeer).mockResolvedValueOnce({ ok: false }); + await expect( + ensureRncpDestinationReachable({ destinationHash: DEST, lxmfPeerHash: DEST }), + ).resolves.toEqual({ status: 'peerUnreachable' }); + expect(probeReticulumPeer).toHaveBeenCalledTimes(1); + }); + + it('returns peerUnreachable for invalid destination hashes without probing', async () => { + await expect( + ensureRncpDestinationReachable({ destinationHash: 'short', lxmfPeerHash: LXMF }), + ).resolves.toEqual({ status: 'peerUnreachable' }); + expect(probeReticulumPeer).not.toHaveBeenCalled(); + }); +}); diff --git a/src/renderer/lib/ensureRncpDestinationReachable.ts b/src/renderer/lib/ensureRncpDestinationReachable.ts new file mode 100644 index 000000000..0803f987d --- /dev/null +++ b/src/renderer/lib/ensureRncpDestinationReachable.ts @@ -0,0 +1,65 @@ +import { probeReticulumPeer } from '@/renderer/lib/reticulum/reticulumSidecarReads'; +import { isCanonicalReticulumDestinationHash } from '@/shared/reticulumDestinationHash'; + +export type RncpDestinationReachability = + | { status: 'reachable'; hops?: number } + | { status: 'listenerLikelyOff' } + | { status: 'peerUnreachable' }; + +export interface EnsureRncpDestinationReachableArgs { + /** rncp.receive (or fetch) destination hash — required path for the transfer. */ + destinationHash: string; + /** + * Peer LXMF delivery hash when known (Chat DM always; Remote when saved). + * Used only when the receive dest probe fails, to distinguish offline vs + * listener-not-announcing. + */ + lxmfPeerHash?: string | null; +} + +/** True when `value` is already trimmed/lowercased 32-char hex (rncp/LXMF dest). */ +export function isRncpHexHash(value: string): boolean { + return isCanonicalReticulumDestinationHash(value); +} + +/** + * Preflight before rncp send/fetch: require a live path to the transfer destination. + * When that fails but LXMF still probes ok, classify as listener-likely-off so the + * UI can prompt an enable-request instead of queuing a stuck 0% transfer. + */ +export async function ensureRncpDestinationReachable( + args: EnsureRncpDestinationReachableArgs, +): Promise { + const destinationHash = args.destinationHash.trim().toLowerCase(); + if (!isRncpHexHash(destinationHash)) { + return { status: 'peerUnreachable' }; + } + + let destProbe: Awaited>; + try { + destProbe = await probeReticulumPeer(destinationHash); + } catch { + // catch-no-log-ok probe failures classify as unreachable for the gate + return { status: 'peerUnreachable' }; + } + if (destProbe.ok) { + return { status: 'reachable', hops: destProbe.hops }; + } + + const lxmf = args.lxmfPeerHash?.trim().toLowerCase() ?? ''; + if (!isRncpHexHash(lxmf) || lxmf === destinationHash) { + return { status: 'peerUnreachable' }; + } + + let lxmfProbe: Awaited>; + try { + lxmfProbe = await probeReticulumPeer(lxmf); + } catch { + // catch-no-log-ok probe failures classify as unreachable for the gate + return { status: 'peerUnreachable' }; + } + if (lxmfProbe.ok) { + return { status: 'listenerLikelyOff' }; + } + return { status: 'peerUnreachable' }; +} diff --git a/src/renderer/locales/cs/translation.json b/src/renderer/locales/cs/translation.json index e7ea79534..b91b65396 100644 --- a/src/renderer/locales/cs/translation.json +++ b/src/renderer/locales/cs/translation.json @@ -600,7 +600,6 @@ "rememberAddress": "Zapamatovat si tuto adresu", "rememberAddressAria": "Zapamatovat si tento cíl RNCP pro tohoto kolegu", "chooseAndSend": "Vyberte soubor a odešlete...", - "sending": "Odesílání...", "sendStarted": "Odesílání {{file}}…", "sendFailed": "Odeslání se nezdařilo: {{error}}", "newOfferToast": "{{peer}} ti chce poslat soubor: {{file}}", @@ -621,7 +620,9 @@ "forgetAddress": "Zapomenout uložený cíl", "forgetAddressAria": "Odebrat uložený cíl příjmu rncp pro tohoto kolegu", "forgotAddressToast": "Zapomněli jste uložený cíl rncp pro tohoto kolegu.", - "filledFromChatToast": "Vyplněný cíl z chatu (neuloženo — zaškrtněte Nezapomeňte jej ponechat)." + "filledFromChatToast": "Vyplněný cíl z chatu (neuloženo — zaškrtněte Nezapomeňte jej ponechat).", + "offerBannerTitle": "Nabídky příchozích souborů", + "offerBannerTitleAria": "Nevyřízené nabídky příchozích souborů od tohoto partnera" }, "shareLocation": "Sdílejte polohu", "shareLocationLabel": "📍 Sdílená poloha", @@ -4881,7 +4882,12 @@ "copyInstructions": "Kopírovat pokyny", "instructionsCopied": "Pokyny zkopírovány.", "receiveDestSharedToast": "Uložen rncp receive destination uživatele {{peer}} — nyní můžete odeslat soubor.", - "receiveDestShareFailed": "Nelze uložit cíl příjmu rncp partnera." + "receiveDestShareFailed": "Nelze uložit cíl příjmu rncp partnera.", + "peerUnreachable": "Žádná cesta k tomuto cíli. Partner může být offline.", + "checkingReachability": "Kontrola dosažitelnosti…", + "listenerLikelyOffTitle": "Příjem souborů může být vypnutý", + "listenerLikelyOffBody": "Žádná cesta k cílovému umístění jejich souboru, ale hledají online chat. Požádejte je, aby povolili příjem souborů (Vzdálené → Nastavení → Nabídky příchozích souborů), a zkuste to znovu.", + "listenerLikelyOffConfirm": "Odeslat požadavek na povolení" }, "enableRequest": { "title": "Povolit příjem souborů?", @@ -4898,9 +4904,9 @@ "enableFailed": "Nelze povolit příjem souboru: {{error}}", "saveDirRequired": "Chcete-li povolit příjem, vyberte složku pro uložení.", "identityUnknown": "Hash identity tohoto peera se zatím nepodařilo zjistit — příchozí dotazování je povoleno, ale peer nebyl přidán na seznam povolených. Zkuste to znovu, až dorazí cesta nebo oznámení.", - "lxmfBody": "Povolte příjem souborů (rncp), pokud používáte mesh-client: Remote → Settings → Inbound file offers. Nebo spusťte rncp/rncp-rs v režimu poslechu.", "lxmfShareBody": "Příjem souborů je povolen. Zde je můj rncp receive destination (mesh-client jej pro vás uloží).", - "shareDestWarning": "Pokud povolíte, mesh-client odešle váš cíl příjmu rncp tomuto partnerovi, aby mohl automaticky vyplnit soubor." + "shareDestWarning": "Pokud povolíte, mesh-client odešle váš cíl příjmu rncp tomuto partnerovi, aby mohl automaticky vyplnit soubor.", + "lxmfBody": "Povolte příjem rncp (Vzdálené → Nastavení → Nabídky příchozích souborů) a pošlete mi svůj hash pro příjem rncp." }, "saved": { "labelPlaceholder": "Označení", diff --git a/src/renderer/locales/de/translation.json b/src/renderer/locales/de/translation.json index 605651f60..126dd4d34 100644 --- a/src/renderer/locales/de/translation.json +++ b/src/renderer/locales/de/translation.json @@ -601,7 +601,6 @@ "rememberAddress": "Diese Adresse merken", "rememberAddressAria": "Merken Sie sich dieses rncp-Ziel für diesen Peer", "chooseAndSend": "Datei auswählen & senden...", - "sending": "Senden…", "sendStarted": "{{file}} wird gesendet...", "sendFailed": "Senden fehlgeschlagen: {{error}}", "newOfferToast": "{{peer}} möchte dir eine Datei senden: {{file}}", @@ -622,7 +621,9 @@ "forgetAddress": "Gespeichertes Ziel vergessen", "forgetAddressAria": "Entfernen Sie das gespeicherte rncp-Empfangsziel für diesen Peer", "forgotAddressToast": "Gespeichertes rncp-Ziel für diesen Peer vergessen.", - "filledFromChatToast": "Gefülltes Ziel aus dem Chat (nicht gespeichert — aktivieren Sie Denken Sie daran, es zu behalten)." + "filledFromChatToast": "Gefülltes Ziel aus dem Chat (nicht gespeichert — aktivieren Sie Denken Sie daran, es zu behalten).", + "offerBannerTitle": "Eingehende Dateiangebote", + "offerBannerTitleAria": "Ausstehende eingehende Dateiangebote von diesem Peer" }, "shareLocation": "Standort teilen", "shareLocationLabel": "📍 Geteilter Standort", @@ -4879,7 +4880,12 @@ "copyInstructions": "Anweisungen kopieren", "instructionsCopied": "Anweisungen kopiert.", "receiveDestSharedToast": "rncp-Empfangsziel von {{peer}} gespeichert — Sie können jetzt eine Datei senden.", - "receiveDestShareFailed": "Das RNCP-Empfangsziel des Peers konnte nicht gespeichert werden." + "receiveDestShareFailed": "Das RNCP-Empfangsziel des Peers konnte nicht gespeichert werden.", + "peerUnreachable": "Kein Weg zu diesem Ziel. Der Peer ist möglicherweise offline.", + "checkingReachability": "Erreichbarkeit prüfen…", + "listenerLikelyOffTitle": "Möglicherweise ist der Dateiempfang deaktiviert", + "listenerLikelyOffBody": "Kein Pfad zu ihrem Dateiempfangsziel, aber sie suchen online nach Chat. Bitten Sie sie, den Dateiempfang zu aktivieren (Remote → Einstellungen → Angebote für eingehende Dateien) und versuchen Sie es dann erneut.", + "listenerLikelyOffConfirm": "Aktivierungsanfrage senden" }, "enableRequest": { "title": "Dateiempfang aktivieren?", @@ -4896,9 +4902,9 @@ "enableFailed": "Dateiempfang konnte nicht aktiviert werden: {{error}}", "saveDirRequired": "Wählen Sie einen Speicherordner aus, um den Empfang zu aktivieren.", "identityUnknown": "Der Identitäts-Hash dieses Peers konnte noch nicht aufgelöst werden — eingehendes Nachfragen ist aktiviert, aber der Peer wurde nicht zur Erlaubnisliste hinzugefügt. Versuche es erneut, sobald ein Pfad oder ein Announce eintrifft.", - "lxmfBody": "Bitte aktivieren Sie den Dateiempfang (rncp), wenn Sie mesh-client: Remote → Settings → Inbound file offers verwenden. Oder führen Sie rncp/rncp-rs im Listen-Modus aus.", "lxmfShareBody": "Dateiempfang ist aktiviert. Hier ist mein rncp-Empfangsziel (mesh-client speichert es für Sie).", - "shareDestWarning": "Wenn Sie diese Option aktivieren, sendet der Mesh-Client Ihr RNCP-Empfangsziel an diesen Peer, damit dieser den Dateiversand automatisch ausfüllen kann." + "shareDestWarning": "Wenn Sie diese Option aktivieren, sendet der Mesh-Client Ihr RNCP-Empfangsziel an diesen Peer, damit dieser den Dateiversand automatisch ausfüllen kann.", + "lxmfBody": "Bitte aktivieren Sie den rncp-Empfang (Remote → Einstellungen → Angebote für eingehende Dateien) und senden Sie mir Ihren rncp-Empfangs-Hash." }, "saved": { "labelPlaceholder": "Etikett", diff --git a/src/renderer/locales/en/translation.json b/src/renderer/locales/en/translation.json index 2ca8805f5..344af3718 100644 --- a/src/renderer/locales/en/translation.json +++ b/src/renderer/locales/en/translation.json @@ -515,12 +515,13 @@ "forgotAddressToast": "Forgot saved rncp destination for this peer.", "filledFromChatToast": "Filled destination from chat (not saved — check Remember to keep it).", "chooseAndSend": "Choose file & send…", - "sending": "Sending…", "sendStarted": "Sending {{file}}…", "sendFailed": "Failed to send: {{error}}", "newOfferToast": "{{peer}} wants to send you a file: {{file}}", "requestEnable": "Request enable", - "requestEnableAria": "Ask this peer to enable file receiving" + "requestEnableAria": "Ask this peer to enable file receiving", + "offerBannerTitle": "Incoming file offers", + "offerBannerTitleAria": "Pending inbound file offers from this peer" }, "closeDm": "Close DM", "resendMessage": "Resend message", @@ -4740,6 +4741,11 @@ "requestEnableSent": "Asked the peer to enable file receiving.", "requestEnableRateLimited": "You already sent a request recently. Try again later.", "requestEnableFailed": "Could not send enable request: {{error}}", + "peerUnreachable": "No path to that destination. The peer may be offline.", + "checkingReachability": "Checking reachability…", + "listenerLikelyOffTitle": "File receiving may be off", + "listenerLikelyOffBody": "No path to their file receive destination, but they look online for chat. Ask them to enable file receiving (Remote → Settings → Inbound file offers), then try again.", + "listenerLikelyOffConfirm": "Send enable request", "offerToast": "Incoming file offer: {{file}}", "copyInstructionsAria": "Copy instructions for enabling file receive", "copyInstructions": "Copy instructions", @@ -4763,7 +4769,7 @@ "enableFailed": "Could not enable file receiving: {{error}}", "saveDirRequired": "Choose a save folder to enable receiving.", "identityUnknown": "Could not resolve this peer's identity hash yet — inbound Ask is enabled, but they were not added to the allow list. Try again after a path or announce arrives.", - "lxmfBody": "Please enable file receiving (rncp) if you use mesh-client: Remote → Settings → Inbound file offers. Or run rncp/rncp-rs in listen mode.", + "lxmfBody": "Please enable rncp receive (Remote → Settings → Inbound file offers) and send me your rncp receive hash.", "lxmfShareBody": "File receiving is enabled. Here is my rncp receive destination (mesh-client will save it for you)." }, "saved": { diff --git a/src/renderer/locales/es/translation.json b/src/renderer/locales/es/translation.json index c031228c7..a7f2958f7 100644 --- a/src/renderer/locales/es/translation.json +++ b/src/renderer/locales/es/translation.json @@ -601,7 +601,6 @@ "rememberAddress": "Recordar esta dirección", "rememberAddressAria": "Recordar este destino rncp para este compañero", "chooseAndSend": "Elegir archivo y enviar...", - "sending": "Enviando", "sendStarted": "Enviando {{file}}…", "sendFailed": "Error al enviar: {{error}}", "newOfferToast": "{{peer}} quiere enviarte un archivo: {{file}}", @@ -622,7 +621,9 @@ "forgetAddress": "Olvidar destino guardado", "forgetAddressAria": "Eliminar el destino de recepción rncp guardado para este par", "forgotAddressToast": "Se ha olvidado el destino rncp guardado para este par.", - "filledFromChatToast": "Destino llenado desde el chat (no guardado — marque Recordar para conservarlo)." + "filledFromChatToast": "Destino llenado desde el chat (no guardado — marque Recordar para conservarlo).", + "offerBannerTitle": "Ofertas de archivos entrantes", + "offerBannerTitleAria": "Ofertas de archivos entrantes pendientes de este par" }, "shareLocation": "Compartir ubicación", "shareLocationLabel": "📍 Ubicación compartida", @@ -4879,7 +4880,12 @@ "copyInstructions": "Copiar instrucciones", "instructionsCopied": "Instrucciones copiadas.", "receiveDestSharedToast": "Se guardó el destino de recepción rncp de {{peer}}: ya puede enviar un archivo.", - "receiveDestShareFailed": "No se pudo guardar el destino de recepción rncp del par." + "receiveDestShareFailed": "No se pudo guardar el destino de recepción rncp del par.", + "peerUnreachable": "No hay camino hacia ese destino. Es posible que el interlocutor esté desconectado.", + "checkingReachability": "Comprobando accesibilidad...", + "listenerLikelyOffTitle": "La recepción de archivos puede estar desactivada", + "listenerLikelyOffBody": "No hay ruta al destino de recepción de su archivo, pero buscan chat en línea. Pídales que habiliten la recepción de archivos (Remoto → Configuración → Ofertas de archivos entrantes) y luego vuelva a intentarlo.", + "listenerLikelyOffConfirm": "Enviar solicitud de habilitación" }, "enableRequest": { "title": "¿Habilitar la recepción de archivos?", @@ -4896,9 +4902,9 @@ "enableFailed": "No se ha podido habilitar la recepción de archivos: {{error}}", "saveDirRequired": "Elija una carpeta de guardado para habilitar la recepción.", "identityUnknown": "Aún no se pudo resolver el hash de identidad de este par — el modo Preguntar está activado, pero no se añadió a la lista de permitidos. Inténtalo de nuevo cuando llegue una ruta o un anuncio.", - "lxmfBody": "Habilite la recepción de archivos (rncp) si utiliza mesh-client: → Configuración remota Ofertas de archivos → entrantes. O ejecute rncp/rncp-rs en modo de escucha.", "lxmfShareBody": "La recepción de archivos está habilitada. Aquí está mi destino de recepción rncp (mesh-client lo guardará para usted).", - "shareDestWarning": "Si lo habilita, mesh-client enviará su destino de recepción rncp a este par para que pueda completar automáticamente el envío del archivo." + "shareDestWarning": "Si lo habilita, mesh-client enviará su destino de recepción rncp a este par para que pueda completar automáticamente el envío del archivo.", + "lxmfBody": "Habilite la recepción de rncp (Remoto → Configuración → Ofertas de archivos entrantes) y envíeme su hash de recepción de rncp." }, "saved": { "labelPlaceholder": "Designación", diff --git a/src/renderer/locales/fr/translation.json b/src/renderer/locales/fr/translation.json index 46fa11c33..5354a5413 100644 --- a/src/renderer/locales/fr/translation.json +++ b/src/renderer/locales/fr/translation.json @@ -601,7 +601,6 @@ "rememberAddress": "Mémoriser cette adresse", "rememberAddressAria": "Mémoriser cette destination rncp pour cet homologue", "chooseAndSend": "Choisissez le fichier et envoyez…", - "sending": "Envoi en cours...", "sendStarted": "Envoi {{file}}…", "sendFailed": "Échec de l'envoi : {{error}}", "newOfferToast": "{{peer}} souhaite vous envoyer un fichier : {{file}}", @@ -622,7 +621,9 @@ "forgetAddress": "Oublier la destination enregistrée", "forgetAddressAria": "Supprimer la destination de réception rncp enregistrée pour cet homologue", "forgotAddressToast": "Destination rncp enregistrée oubliée pour cet homologue.", - "filledFromChatToast": "Destination remplie à partir du chat (non enregistrée — cochez la case N'oubliez pas de la conserver)." + "filledFromChatToast": "Destination remplie à partir du chat (non enregistrée — cochez la case N'oubliez pas de la conserver).", + "offerBannerTitle": "Offres de fichiers entrants", + "offerBannerTitleAria": "Offres de fichiers entrants en attente de cet homologue" }, "shareLocation": "Partager l'emplacement", "shareLocationLabel": "📍 Emplacement partagé", @@ -4879,7 +4880,12 @@ "copyInstructions": "Copier les instructions", "instructionsCopied": "Instructions copiées.", "receiveDestSharedToast": "Destination de réception rncp de {{peer}} enregistrée — vous pouvez envoyer un fichier.", - "receiveDestShareFailed": "Impossible d'enregistrer la destination de réception RNCP du homologue." + "receiveDestShareFailed": "Impossible d'enregistrer la destination de réception RNCP du homologue.", + "peerUnreachable": "Aucun chemin vers cette destination. L'homologue est peut-être hors ligne.", + "checkingReachability": "Vérification de l'accessibilité…", + "listenerLikelyOffTitle": "La réception de fichiers est peut-être désactivée", + "listenerLikelyOffBody": "Aucun chemin vers la destination de réception de leur fichier, mais ils recherchent en ligne pour discuter. Demandez-leur d'activer la réception de fichiers (À distance → Paramètres → Offres de fichiers entrants), puis réessayez.", + "listenerLikelyOffConfirm": "Envoyer une demande d'activation" }, "enableRequest": { "title": "Activer la réception de fichiers ?", @@ -4896,9 +4902,9 @@ "enableFailed": "Impossible d'activer la réception du fichier : {{error}}", "saveDirRequired": "Choisissez un dossier de sauvegarde pour activer la réception.", "identityUnknown": "Impossible de résoudre le hachage d'identité de ce pair pour le moment — le mode Demander est activé, mais il n'a pas été ajouté à la liste d'autorisation. Réessayez après l'arrivée d'un chemin ou d'une annonce.", - "lxmfBody": "Veuillez activer la réception de fichiers (rncp) si vous utilisez mesh-client : Remote → Settings → Inbound file offers. Ou exécutez rncp/rncp-rs en mode écoute.", "lxmfShareBody": "La réception de fichiers est activée. Voici ma destination de réception rncp (mesh-client l'enregistrera pour vous).", - "shareDestWarning": "Si vous l'activez, mesh-client enverra votre destination de réception rncp à cet homologue afin qu'il puisse remplir automatiquement l'envoi de fichiers." + "shareDestWarning": "Si vous l'activez, mesh-client enverra votre destination de réception rncp à cet homologue afin qu'il puisse remplir automatiquement l'envoi de fichiers.", + "lxmfBody": "Veuillez activer la réception rncp (À distance → Paramètres → Offres de fichiers entrants) et envoyez-moi votre hachage de réception rncp." }, "saved": { "labelPlaceholder": "Dénomination", diff --git a/src/renderer/locales/id/translation.json b/src/renderer/locales/id/translation.json index 09d4b5b37..66640ee78 100644 --- a/src/renderer/locales/id/translation.json +++ b/src/renderer/locales/id/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "Ingat alamat ini", "rememberAddressAria": "Ingat tujuan rncp untuk rekan ini", "chooseAndSend": "Pilih file & kirim…", - "sending": "Mengirim…", "sendStarted": "Mengirim {{file}}…", "sendFailed": "Gagal mengirim: {{error}}", "newOfferToast": "{{peer}} ingin mengirimi Anda file: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "Lupakan tujuan tersimpan", "forgetAddressAria": "Hapus tujuan penerimaan rncp yang disimpan untuk rekan ini", "forgotAddressToast": "Lupa menyimpan tujuan rncp untuk rekan ini.", - "filledFromChatToast": "Tujuan yang diisi dari obrolan (tidak disimpan — centang Ingat untuk menyimpannya)." + "filledFromChatToast": "Tujuan yang diisi dari obrolan (tidak disimpan — centang Ingat untuk menyimpannya).", + "offerBannerTitle": "Penawaran file masuk", + "offerBannerTitleAria": "Penawaran file masuk yang tertunda dari rekan ini" }, "shareLocation": "Bagikan lokasi", "shareLocationLabel": "📍 Lokasi bersama", @@ -4879,7 +4880,12 @@ "copyInstructions": "Salin instruksi", "instructionsCopied": "Instruksi disalin.", "receiveDestSharedToast": "Destinasi penerimaan rncp {{peer}} disimpan — Anda dapat mengirim file sekarang.", - "receiveDestShareFailed": "Tidak dapat menyimpan tujuan penerimaan rncp rekan." + "receiveDestShareFailed": "Tidak dapat menyimpan tujuan penerimaan rncp rekan.", + "peerUnreachable": "Tidak ada jalan menuju tujuan itu. Rekan tersebut mungkin sedang offline.", + "checkingReachability": "Memeriksa jangkauan…", + "listenerLikelyOffTitle": "Penerimaan file mungkin tidak aktif", + "listenerLikelyOffBody": "Tidak ada jalur ke tujuan penerimaan file mereka, tetapi mereka mencari obrolan online. Minta mereka untuk mengaktifkan penerimaan file (Jarak Jauh → Pengaturan → Penawaran file masuk), lalu coba lagi.", + "listenerLikelyOffConfirm": "Kirim permintaan pengaktifan" }, "enableRequest": { "title": "Aktifkan penerimaan file?", @@ -4896,9 +4902,9 @@ "enableFailed": "Tidak dapat mengaktifkan penerimaan file: {{error}}", "saveDirRequired": "Pilih folder penyimpanan untuk mengaktifkan penerimaan.", "identityUnknown": "Hash identitas peer ini belum dapat ditentukan — mode Tanya untuk file masuk sudah aktif, tetapi peer belum ditambahkan ke daftar izin. Coba lagi setelah jalur atau pengumuman diterima.", - "lxmfBody": "Harap aktifkan penerimaan file (rncp) jika Anda menggunakan mesh-client: Remote → Settings → Inbound file offers. Atau jalankan rncp/rncp-rs dalam mode mendengarkan.", "lxmfShareBody": "Penerimaan file diaktifkan. Ini tujuan penerimaan rncp saya (mesh-client akan menyimpannya untuk Anda).", - "shareDestWarning": "Jika Anda mengaktifkannya, mesh-client akan mengirimkan tujuan penerimaan rncp Anda ke rekan ini sehingga mereka dapat mengisi otomatis pengiriman file." + "shareDestWarning": "Jika Anda mengaktifkannya, mesh-client akan mengirimkan tujuan penerimaan rncp Anda ke rekan ini sehingga mereka dapat mengisi otomatis pengiriman file.", + "lxmfBody": "Harap aktifkan penerimaan rncp (Jarak Jauh → Pengaturan → Penawaran file masuk) dan kirimkan saya hash penerimaan rncp Anda." }, "saved": { "labelPlaceholder": "Label", diff --git a/src/renderer/locales/it/translation.json b/src/renderer/locales/it/translation.json index 59322e33e..80cdecb37 100644 --- a/src/renderer/locales/it/translation.json +++ b/src/renderer/locales/it/translation.json @@ -601,7 +601,6 @@ "rememberAddress": "Ricorda questo indirizzo", "rememberAddressAria": "Ricorda questa destinazione rncp per questo peer", "chooseAndSend": "Scegli file e invia...", - "sending": "Invio in corso…", "sendStarted": "Invio di {{file}} in corso...", "sendFailed": "Invio non riuscito: {{error}}", "newOfferToast": "{{peer}} vuole inviarle un file: {{file}}", @@ -622,7 +621,9 @@ "forgetAddress": "Dimentica la destinazione salvata", "forgetAddressAria": "Rimuovi la destinazione di ricezione rncp salvata per questo peer", "forgotAddressToast": "Ho dimenticato la destinazione rncp salvata per questo peer.", - "filledFromChatToast": "Destinazione riempita dalla chat (non salvata — seleziona Ricordati di conservarla)." + "filledFromChatToast": "Destinazione riempita dalla chat (non salvata — seleziona Ricordati di conservarla).", + "offerBannerTitle": "Offerte di file in arrivo", + "offerBannerTitleAria": "Offerte di file in entrata in sospeso da questo peer" }, "shareLocation": "Condividi posizione", "shareLocationLabel": "📍Posizione condivisa", @@ -4879,7 +4880,12 @@ "copyInstructions": "Copia istruzioni", "instructionsCopied": "Istruzioni copiate.", "receiveDestSharedToast": "Destinazione di ricezione rncp di {{peer}} salvata: ora puoi inviare un file.", - "receiveDestShareFailed": "Impossibile salvare la destinazione di ricezione rncp del peer." + "receiveDestShareFailed": "Impossibile salvare la destinazione di ricezione rncp del peer.", + "peerUnreachable": "Nessun percorso per quella destinazione. Il peer potrebbe essere offline.", + "checkingReachability": "Verifica della raggiungibilità…", + "listenerLikelyOffTitle": "La ricezione dei file potrebbe essere disattivata", + "listenerLikelyOffBody": "Nessun percorso verso la destinazione di ricezione del file, ma cercano online la chat. Chiedi loro di abilitare la ricezione dei file (Telecomando → Impostazioni → Offerte di file in entrata), quindi riprova.", + "listenerLikelyOffConfirm": "Invia richiesta di abilitazione" }, "enableRequest": { "title": "Abilitare la ricezione dei file?", @@ -4896,9 +4902,9 @@ "enableFailed": "Impossibile abilitare la ricezione dei file: {{error}}", "saveDirRequired": "Scegli una cartella di salvataggio per abilitare la ricezione.", "identityUnknown": "Non è stato ancora possibile risolvere l'hash di identità di questo peer — la modalità Chiedi è attiva, ma il peer non è stato aggiunto alla lista dei consentiti. Riprova dopo l'arrivo di un percorso o di un annuncio.", - "lxmfBody": "Abilitare la ricezione dei file (rncp) se si utilizzano le offerte di file mesh-client: Remote → Settings → Inbound. Oppure esegui rncp/rncp-rs in modalità di ascolto.", "lxmfShareBody": "La ricezione file è abilitata. Ecco la mia destinazione di ricezione rncp (mesh-client la salverà per te).", - "shareDestWarning": "Se abiliti, mesh-client invierà la destinazione di ricezione rncp a questo peer in modo che possa compilare automaticamente l'invio del file." + "shareDestWarning": "Se abiliti, mesh-client invierà la destinazione di ricezione rncp a questo peer in modo che possa compilare automaticamente l'invio del file.", + "lxmfBody": "Abilita la ricezione rncp (Telecomando → Impostazioni → Offerte file in entrata) e inviami il tuo hash di ricezione rncp." }, "saved": { "labelPlaceholder": "Label", diff --git a/src/renderer/locales/ja/translation.json b/src/renderer/locales/ja/translation.json index 15ce4a5e5..7dbf02808 100644 --- a/src/renderer/locales/ja/translation.json +++ b/src/renderer/locales/ja/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "この住所を記憶する", "rememberAddressAria": "このピアのこのrncp宛先を覚えておいてください", "chooseAndSend": "ファイルを選択して送信…", - "sending": "送信中・・・", "sendStarted": "{{file}}を送信中…", "sendFailed": "次の送信に失敗しました: {{error}}", "newOfferToast": "{{peer}}様からファイルのお送りをご希望です: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "保存された行き先を忘れる", "forgetAddressAria": "このピアの保存されたrncp受信先を削除します", "forgotAddressToast": "このピアの保存されたrncp宛先を忘れました。", - "filledFromChatToast": "チャットから移動先を入力します(保存されていません。忘れずに保存するようにチェックしてください)。" + "filledFromChatToast": "チャットから移動先を入力します(保存されていません。忘れずに保存するようにチェックしてください)。", + "offerBannerTitle": "受信ファイルオファー", + "offerBannerTitleAria": "このピアからの保留中の受信ファイル オファー" }, "shareLocation": "位置情報を共有する", "shareLocationLabel": "📍 共有場所", @@ -4879,7 +4880,12 @@ "copyInstructions": "手順をコピー", "instructionsCopied": "手順をコピーしました。", "receiveDestSharedToast": "{{peer}} の rncp 受信先を保存しました — ファイルを送信できます。", - "receiveDestShareFailed": "ピアの rncp 受信先を保存できませんでした。" + "receiveDestShareFailed": "ピアの rncp 受信先を保存できませんでした。", + "peerUnreachable": "その目的地への道はありません。ピアがオフラインである可能性があります。", + "checkingReachability": "到達可能性を確認しています…", + "listenerLikelyOffTitle": "ファイル受信がオフになっている可能性があります", + "listenerLikelyOffBody": "ファイルの受信先へのパスはありませんが、オンラインでチャットを探しています。ファイル受信を有効にするよう依頼して (リモート → 設定 → 受信ファイル オファー)、もう一度試してください。", + "listenerLikelyOffConfirm": "有効化リクエストを送信する" }, "enableRequest": { "title": "ファイル受信を有効にしますか?", @@ -4896,9 +4902,9 @@ "enableFailed": "ファイル受信を有効にできませんでした: {{error}}", "saveDirRequired": "受信を有効にする保存フォルダを選択します。", "identityUnknown": "このピアのIDハッシュをまだ解決できませんでした。受信時の確認は有効になりましたが、許可リストには追加されていません。パスまたはアナウンスの到着後にもう一度お試しください。", - "lxmfBody": "mesh-clientをご利用の場合はファイル受信(rncp)を有効にしてください: Remote → Settings → Inbound file offers。またはリッスンモードで rncp/rncp-rs を実行してください。", "lxmfShareBody": "ファイル受信が有効です。これが私の rncp 受信先です(mesh-client が保存します)。", - "shareDestWarning": "有効にすると、mesh-client は rncp 受信宛先をこのピアに送信し、ファイル送信を自動入力できるようになります。" + "shareDestWarning": "有効にすると、mesh-client は rncp 受信宛先をこのピアに送信し、ファイル送信を自動入力できるようになります。", + "lxmfBody": "rncp 受信を有効にして (リモート → 設定 → インバウンド ファイル オファー)、rncp 受信ハッシュを送信してください。" }, "saved": { "labelPlaceholder": "ラベル", diff --git a/src/renderer/locales/ko/translation.json b/src/renderer/locales/ko/translation.json index 0d3c459c9..c16711198 100644 --- a/src/renderer/locales/ko/translation.json +++ b/src/renderer/locales/ko/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "이 주소를 기억하세요", "rememberAddressAria": "이 피어에 대한 이 rncp 대상을 기억하십시오.", "chooseAndSend": "파일을 선택하고 보내세요...", - "sending": "배상…", "sendStarted": "{{file}} 보내는 중…", "sendFailed": "전송 실패: {{error}}", "newOfferToast": "{{peer}}이(가) 당신에게 파일을 보내려고 합니다: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "저장된 목적지 잊음", "forgetAddressAria": "이 피어에 대해 저장된 rncp 수신 대상을 제거합니다", "forgotAddressToast": "이 피어의 저장된 rncp 대상을 잊어버렸습니다.", - "filledFromChatToast": "채팅에서 목적지를 채웠습니다 (저장되지 않음 — 잊지 말고 보관하세요)." + "filledFromChatToast": "채팅에서 목적지를 채웠습니다 (저장되지 않음 — 잊지 말고 보관하세요).", + "offerBannerTitle": "수신 파일 제안", + "offerBannerTitleAria": "이 피어로부터 보류 중인 인바운드 파일 제안" }, "shareLocation": "위치 공유", "shareLocationLabel": "📍 공유 위치", @@ -4879,7 +4880,12 @@ "copyInstructions": "지침 복사", "instructionsCopied": "지침이 복사되었습니다.", "receiveDestSharedToast": "{{peer}}의 rncp 수신 대상을 저장했습니다 — 이제 파일을 보낼 수 있습니다.", - "receiveDestShareFailed": "피어의 rncp 수신 대상을 저장할 수 없습니다." + "receiveDestShareFailed": "피어의 rncp 수신 대상을 저장할 수 없습니다.", + "peerUnreachable": "해당 목적지까지의 경로가 없습니다. 피어가 오프라인일 수 있습니다.", + "checkingReachability": "연결 가능성 확인 중…", + "listenerLikelyOffTitle": "파일 수신이 꺼져 있을 수 있습니다.", + "listenerLikelyOffBody": "파일 수신 대상에 대한 경로가 없지만 온라인에서 채팅을 찾습니다. 파일 수신 활성화(원격 → 설정 → 인바운드 파일 제공)를 요청한 후 다시 시도하세요.", + "listenerLikelyOffConfirm": "활성화 요청 보내기" }, "enableRequest": { "title": "파일 수신을 활성화하시겠습니까?", @@ -4896,9 +4902,9 @@ "enableFailed": "파일 수신을 활성화할 수 없습니다: {{error}}", "saveDirRequired": "수신을 활성화하려면 저장 폴더를 선택하세요.", "identityUnknown": "이 피어의 신원 해시를 아직 확인할 수 없습니다 — 수신 확인은 활성화되었지만 허용 목록에는 추가되지 않았습니다. 경로나 알림이 도착한 후 다시 시도하세요.", - "lxmfBody": "mesh-client를 사용하는 경우 파일 수신(rncp)을 활성화하십시오: 원격 → 설정 → 인바운드 파일 제공. 또는 청취 모드에서 rncp/rncp-rs를 실행하십시오.", "lxmfShareBody": "파일 수신이 활성화되었습니다. 제 rncp 수신 대상입니다(mesh-client가 저장합니다).", - "shareDestWarning": "활성화하면 mesh-client는 rncp 수신 대상을 이 피어에 보내 파일 보내기를 자동 채울 수 있습니다." + "shareDestWarning": "활성화하면 mesh-client는 rncp 수신 대상을 이 피어에 보내 파일 보내기를 자동 채울 수 있습니다.", + "lxmfBody": "rncp 수신을 활성화하고(원격 → 설정 → 인바운드 파일 제공) rncp 수신 해시를 보내주세요." }, "saved": { "labelPlaceholder": "상표", diff --git a/src/renderer/locales/nl/translation.json b/src/renderer/locales/nl/translation.json index d9be70013..18f7bc39e 100644 --- a/src/renderer/locales/nl/translation.json +++ b/src/renderer/locales/nl/translation.json @@ -600,7 +600,6 @@ "rememberAddress": "Dit adres onthouden", "rememberAddressAria": "Onthoud deze rncp-bestemming voor deze collega", "chooseAndSend": "Bestand kiezen en verzenden...", - "sending": "Versturen...", "sendStarted": "{{file}} verzenden…", "sendFailed": "Verzenden mislukt: {{error}}", "newOfferToast": "{{peer}} wil je een bestand sturen: {{file}}", @@ -621,7 +620,9 @@ "forgetAddress": "Opgeslagen bestemming vergeten", "forgetAddressAria": "Verwijder de opgeslagen rncp-ontvangstbestemming voor deze peer", "forgotAddressToast": "Opgeslagen rncp-bestemming voor deze peer vergeten.", - "filledFromChatToast": "Gevuld doel van chat (niet opgeslagen — vink Vergeet niet om het te bewaren)." + "filledFromChatToast": "Gevuld doel van chat (niet opgeslagen — vink Vergeet niet om het te bewaren).", + "offerBannerTitle": "Inkomende bestandsaanbiedingen", + "offerBannerTitleAria": "In afwachting van inkomende bestandsaanbiedingen van deze peer" }, "shareLocation": "Deel locatie", "shareLocationLabel": "📍 Gedeelde locatie", @@ -4879,7 +4880,12 @@ "copyInstructions": "Kopieer instructies", "instructionsCopied": "Instructies gekopieerd.", "receiveDestSharedToast": "rncp-ontvangstbestemming van {{peer}} opgeslagen — u kunt nu een bestand verzenden.", - "receiveDestShareFailed": "Kan de rncp-ontvangstbestemming van de peer niet opslaan." + "receiveDestShareFailed": "Kan de rncp-ontvangstbestemming van de peer niet opslaan.", + "peerUnreachable": "Geen pad naar die bestemming. De peer is mogelijk offline.", + "checkingReachability": "Bereikbaarheid controleren...", + "listenerLikelyOffTitle": "Het ontvangen van bestanden is mogelijk uitgeschakeld", + "listenerLikelyOffBody": "Er is geen pad naar de bestemming van hun bestand, maar ze zoeken online naar chat. Vraag hen om het ontvangen van bestanden in te schakelen (Op afstand → Instellingen → Inkomende bestandsaanbiedingen) en probeer het vervolgens opnieuw.", + "listenerLikelyOffConfirm": "Inschakelverzoek verzenden" }, "enableRequest": { "title": "Bestandsontvangst inschakelen?", @@ -4896,9 +4902,9 @@ "enableFailed": "Kon het ontvangen van bestanden niet inschakelen: {{error}}", "saveDirRequired": "Kies een opslagmap om ontvangst in te schakelen.", "identityUnknown": "De identiteitshash van deze peer kon nog niet worden bepaald — inkomend vragen is ingeschakeld, maar de peer is niet aan de toestaanlijst toegevoegd. Probeer het opnieuw nadat een pad of aankondiging is ontvangen.", - "lxmfBody": "Schakel bestandsontvangst (rncp) in als u mesh-client gebruikt: Extern → Instellingen → Inkomende bestandsaanbiedingen. Of voer rncp/rncp-rs uit in de luistermodus.", "lxmfShareBody": "Bestandsontvangst is ingeschakeld. Dit is mijn rncp-ontvangstbestemming (mesh-client bewaart dit voor u).", - "shareDestWarning": "Als u dit inschakelt, zal mesh-client uw rncp-ontvangstbestemming naar deze peer sturen, zodat zij het verzenden van bestanden automatisch kunnen aanvullen." + "shareDestWarning": "Als u dit inschakelt, zal mesh-client uw rncp-ontvangstbestemming naar deze peer sturen, zodat zij het verzenden van bestanden automatisch kunnen aanvullen.", + "lxmfBody": "Schakel rncp-ontvangst in (Op afstand → Instellingen → Inkomende bestandsaanbiedingen) en stuur mij uw rncp-ontvangst-hash." }, "saved": { "labelPlaceholder": "Label", diff --git a/src/renderer/locales/pl/translation.json b/src/renderer/locales/pl/translation.json index 48107aa3e..13d8ce3b3 100644 --- a/src/renderer/locales/pl/translation.json +++ b/src/renderer/locales/pl/translation.json @@ -601,7 +601,6 @@ "rememberAddress": "Zapamiętaj ten adres", "rememberAddressAria": "Zapamiętaj to miejsce docelowe rncp dla tego partnera", "chooseAndSend": "Wybierz plik i wyślij...", - "sending": "Wysyłanie…", "sendStarted": "Wysyłanie: {{file}}…", "sendFailed": "Nie udało się wysłać: {{error}}", "newOfferToast": "{{peer}} chce wysłać Ci plik: {{file}}", @@ -624,7 +623,9 @@ "forgetAddress": "Zapomnij o zapisanym miejscu docelowym", "forgetAddressAria": "Usuń zapisane miejsce docelowe odbioru rncp dla tego partnera", "forgotAddressToast": "Nie pamiętasz zapisanego miejsca docelowego rncp dla tego partnera.", - "filledFromChatToast": "Wypełnione miejsce docelowe z czatu (niezapisane — zaznacz Pamiętaj, aby je zachować)." + "filledFromChatToast": "Wypełnione miejsce docelowe z czatu (niezapisane — zaznacz Pamiętaj, aby je zachować).", + "offerBannerTitle": "Przychodzące oferty plików", + "offerBannerTitleAria": "Oczekujące oferty plików przychodzących od tego partnera" }, "shareLocation": "Udostępnij lokalizację", "shareLocationLabel": "📍 Udostępniona lokalizacja", @@ -4883,7 +4884,12 @@ "copyInstructions": "Kopiuj instrukcje", "instructionsCopied": "Skopiowano instrukcje.", "receiveDestSharedToast": "Zapisano miejsce docelowe odbioru rncp użytkownika {{peer}} — możesz teraz wysłać plik.", - "receiveDestShareFailed": "Nie można zapisać miejsca docelowego odbioru rncp urządzenia równorzędnego." + "receiveDestShareFailed": "Nie można zapisać miejsca docelowego odbioru rncp urządzenia równorzędnego.", + "peerUnreachable": "Żadnej drogi do tego celu. Partner może być offline.", + "checkingReachability": "Sprawdzam osiągalność…", + "listenerLikelyOffTitle": "Odbieranie plików może być wyłączone", + "listenerLikelyOffBody": "Brak ścieżki do miejsca docelowego odbioru pliku, ale szukają czatu w Internecie. Poproś o włączenie odbierania plików (Zdalne → Ustawienia → Oferty plików przychodzących), a następnie spróbuj ponownie.", + "listenerLikelyOffConfirm": "Wyślij prośbę o włączenie" }, "enableRequest": { "title": "Włączyć odbiór plików?", @@ -4900,9 +4906,9 @@ "enableFailed": "Nie można włączyć odbierania plików: {{error}}", "saveDirRequired": "Wybierz folder zapisu, aby włączyć odbiór.", "identityUnknown": "Nie udało się jeszcze ustalić skrótu tożsamości tego peera — tryb pytania jest włączony, ale nie dodano go do listy dozwolonych. Spróbuj ponownie, gdy nadejdzie ścieżka lub ogłoszenie.", - "lxmfBody": "Włącz odbieranie plików (rncp), jeśli używasz funkcji mesh-client: Remote → Settings → Inbound file offers. Lub uruchom rncp/rncp-rs w trybie słuchania.", "lxmfShareBody": "Odbieranie plików jest włączone. Oto moje miejsce docelowe odbioru rncp (mesh-client zapisze je dla Ciebie).", - "shareDestWarning": "Jeśli włączysz, mesh-client wyśle miejsce docelowe odbioru rncp do tego partnera, aby mógł automatycznie wypełnić wysyłanie pliku." + "shareDestWarning": "Jeśli włączysz, mesh-client wyśle miejsce docelowe odbioru rncp do tego partnera, aby mógł automatycznie wypełnić wysyłanie pliku.", + "lxmfBody": "Włącz odbieranie rncp (Zdalne → Ustawienia → Oferty plików przychodzących) i wyślij mi swój skrót odbierania rncp." }, "saved": { "labelPlaceholder": "Oznaczenie", diff --git a/src/renderer/locales/pt-BR/translation.json b/src/renderer/locales/pt-BR/translation.json index c1602c42a..ebb1870f7 100644 --- a/src/renderer/locales/pt-BR/translation.json +++ b/src/renderer/locales/pt-BR/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "Lembrar este endereço", "rememberAddressAria": "Lembre-se deste destino rncp para este par", "chooseAndSend": "Escolher arquivo e enviar...", - "sending": "Enviando…", "sendStarted": "Enviando {{file}}…", "sendFailed": "Falha ao enviar: {{error}}", "newOfferToast": "{{peer}} deseja enviar-lhe um arquivo: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "Esqueça o destino guardado", "forgetAddressAria": "Remover o destino de recebimento rncp salvo para este par", "forgotAddressToast": "Esqueci o destino rncp salvo para este par.", - "filledFromChatToast": "Destino preenchido do chat (não salvo — marque Lembre-se de mantê-lo)." + "filledFromChatToast": "Destino preenchido do chat (não salvo — marque Lembre-se de mantê-lo).", + "offerBannerTitle": "Ofertas de arquivos recebidos", + "offerBannerTitleAria": "Ofertas de arquivos de entrada pendentes deste peer" }, "shareLocation": "Compartilhar localização", "shareLocationLabel": "📍 Localização compartilhada", @@ -4879,7 +4880,12 @@ "copyInstructions": "Copiar instruções", "instructionsCopied": "Instruções copiadas.", "receiveDestSharedToast": "Destino de recebimento rncp de {{peer}} salvo — você já pode enviar um arquivo.", - "receiveDestShareFailed": "Não foi possível salvar o destino de recebimento do rncp do par." + "receiveDestShareFailed": "Não foi possível salvar o destino de recebimento do rncp do par.", + "peerUnreachable": "Nenhum caminho para esse destino. O par pode estar offline.", + "checkingReachability": "Verificando acessibilidade…", + "listenerLikelyOffTitle": "O recebimento de arquivos pode estar desativado", + "listenerLikelyOffBody": "Nenhum caminho para o destino de recebimento do arquivo, mas eles procuram bate-papo on-line. Peça-lhes para ativar o recebimento de arquivos (Remoto → Configurações → Ofertas de arquivos de entrada) e tente novamente.", + "listenerLikelyOffConfirm": "Enviar solicitação de ativação" }, "enableRequest": { "title": "Habilitar recebimento de arquivos?", @@ -4896,9 +4902,9 @@ "enableFailed": "Não foi possível ativar o recebimento de arquivos: {{error}}", "saveDirRequired": "Escolha uma pasta de salvamento para ativar o recebimento.", "identityUnknown": "Ainda não foi possível resolver o hash de identidade deste par — o modo Perguntar foi ativado, mas ele não foi adicionado à lista de permitidos. Tente novamente quando um caminho ou anúncio chegar.", - "lxmfBody": "Ative o recebimento de arquivos (rncp) se você usar mesh-client: Remote → Settings → Inbound file offers. Ou execute rncp/rncp-rs no modo de escuta.", "lxmfShareBody": "O recebimento de arquivos está ativado. Aqui está meu destino de recebimento rncp (mesh-client salvará para você).", - "shareDestWarning": "Se você ativar, o mesh-client enviará seu destino de recebimento rncp para esse par para que eles possam preencher automaticamente o envio do arquivo." + "shareDestWarning": "Se você ativar, o mesh-client enviará seu destino de recebimento rncp para esse par para que eles possam preencher automaticamente o envio do arquivo.", + "lxmfBody": "Ative o recebimento do rncp (Remoto → Configurações → Ofertas de arquivos de entrada) e envie-me seu hash de recebimento do rncp." }, "saved": { "labelPlaceholder": "Etiqueta", diff --git a/src/renderer/locales/ru/translation.json b/src/renderer/locales/ru/translation.json index 2ea33da06..f9578c1aa 100644 --- a/src/renderer/locales/ru/translation.json +++ b/src/renderer/locales/ru/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "Запомнить этот адрес", "rememberAddressAria": "Запомнить это назначение rncp для этого однорангового узла", "chooseAndSend": "Выберите файл и отправьте...", - "sending": "отправка…", "sendStarted": "Отправка {{file}}…", "sendFailed": "Не удалось отправить: {{error}}", "newOfferToast": "{{peer}} хочет отправить вам файл: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "Забыть сохраненный пункт назначения", "forgetAddressAria": "Удалить сохраненный пункт назначения получения rncp для этого узла", "forgotAddressToast": "Забыт сохраненный пункт назначения rncp для этого однорангового узла.", - "filledFromChatToast": "Заполненный пункт назначения из чата (не сохранен — отметьте Не забудьте сохранить его)." + "filledFromChatToast": "Заполненный пункт назначения из чата (не сохранен — отметьте Не забудьте сохранить его).", + "offerBannerTitle": "Входящие файловые предложения", + "offerBannerTitleAria": "Ожидающие предложения входящих файлов от этого узла" }, "shareLocation": "Поделиться местоположением", "shareLocationLabel": "📍 Общая локация", @@ -4881,7 +4882,12 @@ "copyInstructions": "Копировать инструкции", "instructionsCopied": "Инструкции скопированы.", "receiveDestSharedToast": "Сохранено rncp-назначение приёма {{peer}} — можно отправить файл.", - "receiveDestShareFailed": "Не удалось сохранить пункт назначения приема rncp узла." + "receiveDestShareFailed": "Не удалось сохранить пункт назначения приема rncp узла.", + "peerUnreachable": "Нет пути к этому месту назначения. Узел может быть отключен от сети.", + "checkingReachability": "Проверка доступности…", + "listenerLikelyOffTitle": "Прием файлов может быть отключен", + "listenerLikelyOffBody": "Нет пути к месту назначения их файла, но они ищут чат в Интернете. Попросите их включить получение файлов (Удаленное управление → Настройки → Предложения входящих файлов), а затем повторите попытку.", + "listenerLikelyOffConfirm": "Отправить запрос на включение" }, "enableRequest": { "title": "Включить получение файлов?", @@ -4898,9 +4904,9 @@ "enableFailed": "Не удалось включить получение файлов: {{error}}", "saveDirRequired": "Выберите папку сохранения, чтобы включить получение.", "identityUnknown": "Пока не удалось определить хеш личности этого пира — режим «Спрашивать» включён, но пир не добавлен в список разрешённых. Повторите попытку после получения пути или анонса.", - "lxmfBody": "Включите получение файлов (rncp), если вы используете mesh-client: Remote → Settings → Inbound file offers. Или запустите rncp/rncp-rs в режиме прослушивания.", "lxmfShareBody": "Приём файлов включён. Вот моё rncp-назначение приёма (mesh-client сохранит его для вас).", - "shareDestWarning": "Если вы включите этот параметр, mesh-client отправит пункт назначения получения rncp этому узлу, чтобы он мог автоматически заполнить файл для отправки." + "shareDestWarning": "Если вы включите этот параметр, mesh-client отправит пункт назначения получения rncp этому узлу, чтобы он мог автоматически заполнить файл для отправки.", + "lxmfBody": "Пожалуйста, включите получение rncp (Удаленное управление → Настройки → Предложения входящих файлов) и пришлите мне хэш получения rncp." }, "saved": { "labelPlaceholder": "Ярлык", diff --git a/src/renderer/locales/tr/translation.json b/src/renderer/locales/tr/translation.json index 1f47cdb0a..89b6e06e1 100644 --- a/src/renderer/locales/tr/translation.json +++ b/src/renderer/locales/tr/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "Bu adresi hatırla", "rememberAddressAria": "Bu eş için bu rncp hedefini hatırla", "chooseAndSend": "Dosya seç ve gönder…", - "sending": "Gönderiliyor…", "sendStarted": "{{file}} gönderiliyor…", "sendFailed": "Gönderilemedi: {{error}}", "newOfferToast": "{{peer}} size bir dosya göndermek istiyor: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "Kaydedilen hedefi unut", "forgetAddressAria": "Bu eş için kaydedilen rncp alma hedefini kaldır", "forgotAddressToast": "Bu eş için kaydedilen rncp hedefini unuttum.", - "filledFromChatToast": "Sohbetten dolu hedef (kaydedilmedi — saklamayı unutmayın seçeneğini işaretleyin)." + "filledFromChatToast": "Sohbetten dolu hedef (kaydedilmedi — saklamayı unutmayın seçeneğini işaretleyin).", + "offerBannerTitle": "Gelen dosya teklifleri", + "offerBannerTitleAria": "Bu eşten gelen dosya teklifleri bekleniyor" }, "shareLocation": "Konumu paylaş", "shareLocationLabel": "📍 Paylaşılan konum", @@ -4879,7 +4880,12 @@ "copyInstructions": "Talimatları kopyala", "instructionsCopied": "Talimatlar kopyalandı.", "receiveDestSharedToast": "{{peer}} için rncp alma hedefi kaydedildi — şimdi dosya gönderebilirsiniz.", - "receiveDestShareFailed": "Eşin rncp alma hedefi kaydedilemedi." + "receiveDestShareFailed": "Eşin rncp alma hedefi kaydedilemedi.", + "peerUnreachable": "O hedefe giden yol yok. Eş çevrimdışı olabilir.", + "checkingReachability": "Erişilebilirlik kontrol ediliyor…", + "listenerLikelyOffTitle": "Dosya alma kapalı olabilir", + "listenerLikelyOffBody": "Dosyalarının varış noktasına giden yolu yok, ancak sohbet için çevrimiçi görünüyorlar. Dosya almayı etkinleştirmelerini isteyin (Uzak → Ayarlar → Gelen dosya teklifleri), ardından tekrar deneyin.", + "listenerLikelyOffConfirm": "Etkinleştirme isteği gönder" }, "enableRequest": { "title": "Dosya alımı etkinleştirilsin mi?", @@ -4896,9 +4902,9 @@ "enableFailed": "Dosya alımı etkinleştirilemedi: {{error}}", "saveDirRequired": "Alımı etkinleştirmek için bir kaydetme klasörü seçin.", "identityUnknown": "Bu eşin kimlik karması henüz çözülemedi — gelen Sor modu etkinleştirildi ancak eş izin listesine eklenmedi. Bir yol veya duyuru geldikten sonra tekrar deneyin.", - "lxmfBody": "Mesh-client kullanıyorsanız lütfen dosya almayı (rncp) etkinleştirin: Uzak → Ayarlar → Gelen dosya teklifleri. Veya rncp/rncp-rs'yi dinleme modunda çalıştırın.", "lxmfShareBody": "Dosya alma etkinleştirildi. İşte rncp alma hedefim (mesh-client sizin için kaydedecek).", - "shareDestWarning": "Etkinleştirirseniz mesh-client, dosya gönderimini otomatik olarak doldurabilmeleri için rncp alma hedefinizi bu eşe gönderecektir." + "shareDestWarning": "Etkinleştirirseniz mesh-client, dosya gönderimini otomatik olarak doldurabilmeleri için rncp alma hedefinizi bu eşe gönderecektir.", + "lxmfBody": "Lütfen rncp alımını etkinleştirin (Uzak → Ayarlar → Gelen dosya teklifleri) ve bana rncp alma karmanızı gönderin." }, "saved": { "labelPlaceholder": "Etiket", diff --git a/src/renderer/locales/uk/translation.json b/src/renderer/locales/uk/translation.json index 6af8b829d..55c736d56 100644 --- a/src/renderer/locales/uk/translation.json +++ b/src/renderer/locales/uk/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "Запам'ятати цю адресу", "rememberAddressAria": "Запам'ятати цю адресу rncp для цього співрозмовника", "chooseAndSend": "Виберіть файл і надішліть…", - "sending": "Заклад,", "sendStarted": "Надсилання {{file}}…", "sendFailed": "Не вдалося надіслати: {{error}}", "newOfferToast": "{{peer}} хоче надіслати вам файл: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "Забути збережений пункт призначення", "forgetAddressAria": "Видалити збережене місце призначення отримання rncp для цього вузла", "forgotAddressToast": "Забув збережений пункт призначення rncp для цього вузла.", - "filledFromChatToast": "Заповнене місце призначення з чату (не збережено — перевірте, щоб зберегти його)." + "filledFromChatToast": "Заповнене місце призначення з чату (не збережено — перевірте, щоб зберегти його).", + "offerBannerTitle": "Пропозиції щодо вхідних файлів", + "offerBannerTitleAria": "Пропозиції щодо вхідних файлів, що очікують на розгляд, від цього партнера" }, "shareLocation": "Поділіться місцем розташування", "shareLocationLabel": "📍 Спільне розташування", @@ -4881,7 +4882,12 @@ "copyInstructions": "Копіювати інструкції", "instructionsCopied": "Інструкції скопійовано.", "receiveDestSharedToast": "Збережено rncp destination прийому {{peer}} — тепер можна надіслати файл.", - "receiveDestShareFailed": "Не вдалося зберегти адресат отримання rncp однорангового пристрою." + "receiveDestShareFailed": "Не вдалося зберегти адресат отримання rncp однорангового пристрою.", + "peerUnreachable": "Немає шляху до цього пункту призначення. Одноранговий пристрій може бути офлайн.", + "checkingReachability": "Перевірка доступності…", + "listenerLikelyOffTitle": "Отримання файлів може бути вимкнено", + "listenerLikelyOffBody": "Немає шляху до пункту призначення файлу, але вони шукають онлайн-чат. Попросіть їх увімкнути отримання файлів (Віддалене → Налаштування → Пропозиції вхідних файлів), а потім повторіть спробу.", + "listenerLikelyOffConfirm": "Надіслати запит на ввімкнення" }, "enableRequest": { "title": "Увімкнути прийом файлів?", @@ -4898,9 +4904,9 @@ "enableFailed": "Не вдалося увімкнути отримання файлу: {{error}}", "saveDirRequired": "Виберіть папку збереження, щоб увімкнути отримання.", "identityUnknown": "Поки не вдалося визначити хеш особи цього піра — режим «Запитувати» увімкнено, але піра не додано до списку дозволених. Спробуйте ще раз після надходження шляху або анонсу.", - "lxmfBody": "Увімкніть отримання файлів (rncp), якщо ви використовуєте mesh-client: Remote → Settings → Inbound file offers. Або запустіть rncp/rncp-rs у режимі прослуховування.", "lxmfShareBody": "Прийом файлів увімкнено. Ось мій rncp destination прийому (mesh-client збереже його для вас).", - "shareDestWarning": "Якщо ввімкнути, mesh-client надсилатиме ваш адресат отримання rncp цьому вузлу, щоб він міг автоматично заповнювати надсилання файлів." + "shareDestWarning": "Якщо ввімкнути, mesh-client надсилатиме ваш адресат отримання rncp цьому вузлу, щоб він міг автоматично заповнювати надсилання файлів.", + "lxmfBody": "Будь ласка, увімкніть отримання rncp (Віддалений → Налаштування → Пропозиції вхідних файлів) і надішліть мені свій хеш отримання rncp." }, "saved": { "labelPlaceholder": "Мітка", diff --git a/src/renderer/locales/zh/translation.json b/src/renderer/locales/zh/translation.json index 4d17209a2..4dc9bfee8 100644 --- a/src/renderer/locales/zh/translation.json +++ b/src/renderer/locales/zh/translation.json @@ -602,7 +602,6 @@ "rememberAddress": "记住此地址", "rememberAddressAria": "记住此节点的此rncp目标", "chooseAndSend": "选择文件并发送…", - "sending": "发送中…", "sendStarted": "正在发送{{file}} …", "sendFailed": "发送失败: {{error}}", "newOfferToast": "{{peer}}想向您发送文件: {{file}}", @@ -623,7 +622,9 @@ "forgetAddress": "忘记已保存的目的地", "forgetAddressAria": "删除此对等体已保存的rncp接收目的地", "forgotAddressToast": "忘记了此对等方的保存的rncp目的地。", - "filledFromChatToast": "通过聊天填写目的地(未保存—选中请记住保留)。" + "filledFromChatToast": "通过聊天填写目的地(未保存—选中请记住保留)。", + "offerBannerTitle": "传入文件报价", + "offerBannerTitleAria": "来自该对等方的待定入站文件报价" }, "shareLocation": "分享位置", "shareLocationLabel": "📍 共享位置", @@ -4879,7 +4880,12 @@ "copyInstructions": "复制说明", "instructionsCopied": "说明已复制。", "receiveDestSharedToast": "已保存 {{peer}} 的 rncp 接收目的地 — 现在可以发送文件。", - "receiveDestShareFailed": "无法保存对等方的 rncp 接收目的地。" + "receiveDestShareFailed": "无法保存对等方的 rncp 接收目的地。", + "peerUnreachable": "没有通往那个目的地的路。对等方可能离线。", + "checkingReachability": "检查可达性...", + "listenerLikelyOffTitle": "文件接收可能已关闭", + "listenerLikelyOffBody": "没有通往文件接收目的地的路径,但他们在网上查找聊天内容。要求他们启用文件接收(远程 → 设置 → 入站文件提供),然后重试。", + "listenerLikelyOffConfirm": "发送启用请求" }, "enableRequest": { "title": "启用文件接收?", @@ -4896,9 +4902,9 @@ "enableFailed": "无法启用文件接收: {{error}}", "saveDirRequired": "选择一个保存文件夹以启用接收。", "identityUnknown": "暂时无法解析此对等节点的身份哈希 — 已启用来件询问,但未将其加入允许列表。等路径或公告到达后再试一次。", - "lxmfBody": "如果您使用mesh-client :远程→设置→入站文件提供,请启用文件接收( rncp )。或者在监听模式下运行rncp/rncp-rs。", "lxmfShareBody": "文件接收已启用。这是我的 rncp 接收目的地(mesh-client 将为您保存)。", - "shareDestWarning": "如果启用,mesh-client 会将您的 rncp 接收目标发送到该对等方,以便他们可以自动填充文件发送。" + "shareDestWarning": "如果启用,mesh-client 会将您的 rncp 接收目标发送到该对等方,以便他们可以自动填充文件发送。", + "lxmfBody": "请启用 rncp 接收(远程 → 设置 → 入站文件提供)并向我发送您的 rncp 接收哈希值。" }, "saved": { "labelPlaceholder": "标号",