@@ -2065,7 +2073,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// Mobile: leave room for the drawer hamburger and shed non-essential header
// chrome so avatar + name + status + call/Disconnect fit a narrow screen.
'@media (max-width:768px){' +
- '.sb-chat-header{padding-left:58px !important;gap:8px !important;}' +
+ '.sb-chat-header{padding-inline-start:58px !important;gap:8px !important;}' +
// Remove the security/verification pill on mobile (per request) — the
// full report is still available from the desktop header.
'.sb-chat-header .sb-secpill{display:none !important;}' +
@@ -2084,7 +2092,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
'.sb-chips .sb-chips-right{flex-wrap:nowrap !important;}' +
'.sb-chips .sb-chip{white-space:nowrap;}' +
'}' +
- '@media (max-width:480px){.sb-chat-header{padding-right:12px !important;}}'
+ '@media (max-width:480px){.sb-chat-header{padding-inline-end:12px !important;}}'
} });
const header = React.createElement('header', {
key: 'hdr', className: 'sb-chat-header', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '24px', padding: '0 20px', height: '64px', borderBottom: '1px solid rgba(255,255,255,0.06)', background: 'rgba(18,18,20,0.72)', backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)' }
@@ -2095,7 +2103,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('div', { key: 'left', style: { display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 } }, [
React.createElement('div', { key: 'avatar', style: { position: 'relative', flex: 'none', width: '36px', height: '36px', borderRadius: '10px', display: 'grid', placeItems: 'center', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.09)', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: '#e8e8eb' } }, [
monoInitials(title || t('chatHdr.chat')),
- React.createElement('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: peerDot, border: '2px solid #121214' } })
+ React.createElement('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: peerDot, border: '2px solid #121214' } })
]),
editingName
? React.createElement('div', { key: 'edit', style: { display: 'flex', flexDirection: 'column', gap: '4px', minWidth: 0 } }, [
@@ -2358,7 +2366,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}, [
React.createElement('button', {
key: 'hs-btn', onClick: () => setShowHandshake(v => !v),
- style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '14px 16px', background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }
+ style: { width: '100%', display: 'flex', alignItems: 'center', gap: '13px', padding: '14px 16px', background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', textAlign: 'start', fontFamily: 'inherit' }
}, [
React.createElement('div', { key: 'ic', style: { flex: 'none', width: '30px', height: '30px', display: 'grid', placeItems: 'center' } },
React.createElement('i', { className: 'fas fa-check', style: { color: '#3ecf8e', fontSize: '16px' } })
@@ -2369,8 +2377,8 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
]),
React.createElement('i', { key: 'chev', className: 'fas fa-chevron-down', style: { flex: 'none', color: '#6b6b73', fontSize: '13px', transform: showHandshake ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .2s' } })
]),
- showHandshake && React.createElement('div', { key: 'hs-body', style: { padding: '2px 16px 14px 59px' } }, [
- systemMessages.length > 0 && React.createElement('div', { key: 'steps', className: 'sb-scroll', style: { marginBottom: '12px', maxHeight: '220px', overflowY: 'auto', paddingRight: '6px' } },
+ showHandshake && React.createElement('div', { key: 'hs-body', style: { paddingTop: '2px', paddingBottom: '14px', paddingInlineStart: '59px', paddingInlineEnd: '16px' } }, [
+ systemMessages.length > 0 && React.createElement('div', { key: 'steps', className: 'sb-scroll', style: { marginBottom: '12px', maxHeight: '220px', overflowY: 'auto', paddingInlineEnd: '6px' } },
systemMessages.map((m, i) => React.createElement('div', { key: 's' + i, style: { display: 'flex', gap: '11px', padding: '6px 0', borderTop: i === 0 ? 'none' : '1px solid rgba(255,255,255,0.04)' } }, [
React.createElement('span', { key: 'd', style: { flex: 'none', width: '5px', height: '5px', borderRadius: '50%', background: '#3ecf8e', marginTop: '7px', opacity: 0.6 } }),
React.createElement('span', { key: 't', style: { flex: 1, fontSize: '12.5px', color: '#9a9aa2', lineHeight: 1.5, wordBreak: 'break-word' } }, String(m.message || '').trim()),
@@ -2425,12 +2433,12 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// ---- option rows ----
const timerRow = showTimer && React.createElement('div', { key: 'timer-row', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', padding: '10px 12px', marginBottom: '10px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618' } },
- [React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginRight: '4px' } }, t('chat.disappearAfter'))].concat(
+ [React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginInlineEnd: '4px' } }, t('chat.disappearAfter'))].concat(
timerDefs.map((d) => React.createElement('button', { key: 'td' + d.v, onClick: () => pickTimer(d.v), style: optStyle(disappearTtl === d.v) }, d.label))
)
);
const onceRow = showOnce && React.createElement('div', { key: 'once-row', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', padding: '10px 12px', marginBottom: '10px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.07)', background: '#161618' } },
- [React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginRight: '4px' } }, t('chat.visibleFor'))].concat(
+ [React.createElement('span', { key: 'lbl', style: { fontSize: '12px', color: '#8a8a92', fontWeight: 600, marginInlineEnd: '4px' } }, t('chat.visibleFor'))].concat(
onceDefs.map((d) => React.createElement('button', { key: 'od' + d.v, onClick: () => pickOnce(d.v), style: optStyle(onceSelected === d.v) }, d.label))
)
);
@@ -2474,14 +2482,14 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const codeStrip = codeMode && React.createElement('div', { key: 'code-strip', style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 14px', border: '1px solid rgba(255,255,255,0.08)', borderBottom: 'none', borderRadius: '14px 14px 0 0', background: '#161618' } }, [
React.createElement('i', { key: 'i', className: 'fas fa-code', style: { color: '#8a8a92', fontSize: '12px' } }),
React.createElement('span', { key: 's', style: { fontSize: '11.5px', fontWeight: 600, color: '#8a8a92' } }, t('chat.codeHint')),
- React.createElement('button', { key: 'c', onClick: () => setCodeMode(false), className: 'sb-link', style: { marginLeft: 'auto', background: 'none', border: 'none', color: '#6b6b73', cursor: 'pointer', fontSize: '11.5px', fontFamily: 'inherit', fontWeight: 600 } }, t('chat.close'))
+ React.createElement('button', { key: 'c', onClick: () => setCodeMode(false), className: 'sb-link', style: { marginInlineStart: 'auto', background: 'none', border: 'none', color: '#6b6b73', cursor: 'pointer', fontSize: '11.5px', fontFamily: 'inherit', fontWeight: 600 } }, t('chat.close'))
]);
// ---- input row ----
const sendBtn = React.createElement('button', {
key: 'send', onClick: onSendMessage, disabled: !hasText, title: t('chat.send'), className: 'sb-send',
style: { flex: 'none', width: '44px', height: '44px', borderRadius: '11px', border: 'none', display: 'grid', placeItems: 'center', cursor: hasText ? 'pointer' : 'default', background: hasText ? '#f0892a' : 'rgba(255,255,255,0.05)', color: hasText ? '#1a0f04' : '#56565e', transition: 'all .15s' }
- }, React.createElement('i', { className: 'fas fa-paper-plane', style: { fontSize: '15px' } }));
+ }, React.createElement('i', { className: 'fas fa-paper-plane sb-mirror-rtl', style: { fontSize: '15px' } }));
const micBtn = React.createElement('button', {
key: 'mic', onClick: () => setIsRecording(true), title: t('chat.recordVoice'), className: 'sb-send',
style: { flex: 'none', width: '44px', height: '44px', borderRadius: '50%', border: 'none', display: 'grid', placeItems: 'center', cursor: 'pointer', background: '#f0892a', color: '#1a0f04', boxShadow: '0 8px 22px rgba(240,137,42,0.3)', transition: 'transform .15s cubic-bezier(.2,.7,.3,1)' }
@@ -2498,7 +2506,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const inputRow = React.createElement('div', {
key: 'input',
- style: { display: 'flex', alignItems: 'flex-end', gap: '11px', padding: '11px 11px 11px 16px', border: '1px solid ' + (hasText ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)'), background: '#161618', borderRadius: codeMode ? '0 0 14px 14px' : '14px', transition: 'border .15s' }
+ style: { display: 'flex', alignItems: 'flex-end', gap: '11px', paddingBlock: '11px', paddingInlineStart: '16px', paddingInlineEnd: '11px', border: '1px solid ' + (hasText ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)'), background: '#161618', borderRadius: codeMode ? '0 0 14px 14px' : '14px', transition: 'border .15s' }
}, [
React.createElement('div', { key: 'ta-wrap', style: { flex: 1, minWidth: 0 } }, [
React.createElement('textarea', {
@@ -2518,7 +2526,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('i', { key: 'i', className: 'fas fa-lock', style: { color: '#3ecf8e', fontSize: '10px' } }),
t('chat.encryptedOnDevice')
]),
- React.createElement('span', { key: 'cnt', style: { fontFamily: MONO, fontSize: '10.5px', color: '#56565e', marginLeft: 'auto' } }, (messageInput ? messageInput.length : 0) + '/2000')
+ React.createElement('span', { key: 'cnt', style: { fontFamily: MONO, fontSize: '10.5px', color: '#56565e', marginInlineStart: 'auto' } }, (messageInput ? messageInput.length : 0) + '/2000')
])
])
].concat(trailingButtons));
@@ -2542,7 +2550,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const scrollBtn = showScrollButton && React.createElement('button', {
key: 'scrollbtn', onClick: handleScrollToBottom,
- style: { position: 'fixed', right: '24px', bottom: '150px', width: '44px', height: '44px', borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)', background: '#26262b', color: '#cfcfd4', display: 'grid', placeItems: 'center', cursor: 'pointer', zIndex: 50, boxShadow: '0 6px 20px rgba(0,0,0,0.4)' }
+ style: { position: 'fixed', insetInlineEnd: '24px', bottom: '150px', width: '44px', height: '44px', borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)', background: '#26262b', color: '#cfcfd4', display: 'grid', placeItems: 'center', cursor: 'pointer', zIndex: 50, boxShadow: '0 6px 20px rgba(0,0,0,0.4)' }
}, React.createElement('i', { className: 'fas fa-arrow-down', style: { fontSize: '15px' } }));
const chatHeader = React.createElement(SecureBitChatHeader, {
@@ -2585,8 +2593,8 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// (72px) — plus a mobile slide-out drawer. Pure presentational: all data comes
// from decorated session objects, all actions are callbacks.
const SB_SVG = {
- chevL: '
',
- chevR: '
',
+ chevL: '
',
+ chevR: '
',
plus: '
',
users: '
',
burger: '
'
@@ -2627,7 +2635,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
offsetRef.current = x;
const w = drawerWidth();
const progress = Math.min(1, Math.max(0, 1 + x / w));
- if (panelRef.current) panelRef.current.style.transform = 'translate3d(' + x.toFixed(2) + 'px,0,0)';
+ if (panelRef.current) panelRef.current.style.transform = 'translate3d(' + (x * DIR).toFixed(2) + 'px,0,0)';
if (scrimRef.current) {
scrimRef.current.style.opacity = progress.toFixed(3);
// The blur comes up with the panel rather than being switched
@@ -2699,7 +2707,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
axis: null,
vel: velocityTracker()
};
- dragRef.current.vel.add(e.clientX, e.timeStamp || performance.now());
+ dragRef.current.vel.add(e.clientX * DIR, e.timeStamp || performance.now());
};
const drawerMove = (e) => {
@@ -2724,9 +2732,9 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
}
try { e.currentTarget.setPointerCapture(d.id); } catch (_) {}
}
- d.vel.add(e.clientX, e.timeStamp || performance.now());
+ d.vel.add(e.clientX * DIR, e.timeStamp || performance.now());
const w = drawerWidth();
- let x = d.base + (e.clientX - d.x0);
+ let x = d.base + (e.clientX - d.x0) * DIR;
// Past the open edge there is nothing left to reveal. Resisting
// reads as "responsive, but this is the end"; stopping dead reads
// as the app having frozen.
@@ -2772,7 +2780,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
const icon = (svg, style) => h('span', { style: Object.assign({ display: 'grid', placeItems: 'center' }, style || {}), dangerouslySetInnerHTML: { __html: svg } });
const avatar = (c, size, ring) => h('div', {
style: { position: 'relative', flex: 'none', width: size + 'px', height: size + 'px', borderRadius: (size >= 44 ? 12 : 11) + 'px', display: 'grid', placeItems: 'center', background: c.active ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.035)', border: '1px solid rgba(255,255,255,' + (c.active ? '0.14' : '0.07') + ')', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: c.active ? '#f4f4f6' : '#9a9aa2' }
- }, [c.mono, h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2px solid ' + ring } })]);
+ }, [c.mono, h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2px solid ' + ring } })]);
// ---- Expanded list row ----
const expandedRow = (c) => h('div', {
@@ -2780,7 +2788,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
onClick: () => onSelect(c.id),
style: { position: 'relative', display: 'flex', alignItems: 'center', gap: '12px', padding: '11px 12px', marginBottom: '4px', borderRadius: '11px', background: c.active ? '#161618' : 'transparent', border: '1px solid ' + (c.active ? 'rgba(255,255,255,0.08)' : 'transparent'), cursor: 'pointer' }
}, [
- c.active && h('span', { key: 'bar', style: { position: 'absolute', left: 0, top: '12px', bottom: '12px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
+ c.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: 0, top: '12px', bottom: '12px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
avatar(c, 38, c.active ? '#161618' : '#0c0c0e'),
h('div', { key: 'body', style: { flex: 1, minWidth: 0 } }, [
h('div', { key: 'top', style: { display: 'flex', alignItems: 'center', gap: '7px' } }, [
@@ -2800,7 +2808,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
// ---- Collapsed dock item ----
const dockItem = (c) => h('div', { key: c.id, style: { position: 'relative' } }, [
- c.active && h('span', { key: 'bar', style: { position: 'absolute', left: '-13px', top: '9px', bottom: '9px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
+ c.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: '-13px', top: '9px', bottom: '9px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
h('div', {
key: 'tile',
onClick: () => onSelect(c.id),
@@ -2808,8 +2816,8 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: c.active ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,' + (c.active ? '0.14' : '0.07') + ')', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: c.active ? '#f4f4f6' : '#9a9aa2' }
}, [
c.mono,
- h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2.5px solid #0c0c0e' } }),
- c.unread && h('span', { key: 'u', style: { position: 'absolute', left: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, c.unread)
+ h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: c.dot, border: '2.5px solid #0c0c0e' } }),
+ c.unread && h('span', { key: 'u', style: { position: 'absolute', insetInlineStart: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, c.unread)
])
]);
@@ -2836,7 +2844,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
...MY_STATUS_OPTIONS.map((o) => h('button', {
key: o.key,
onClick: () => { onSetStatus(o.key); setPresenceOpen(false); },
- style: { width: '100%', display: 'flex', alignItems: 'center', gap: '11px', padding: '9px 10px', borderRadius: '9px', border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left' }
+ style: { width: '100%', display: 'flex', alignItems: 'center', gap: '11px', padding: '9px 10px', borderRadius: '9px', border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'start' }
}, [
h('span', { key: 'd', style: { flex: 'none', width: '10px', height: '10px', borderRadius: '50%', background: o.dot } }),
h('span', { key: 't', style: { flex: 1, minWidth: 0 } }, [
@@ -2855,9 +2863,9 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
h('button', { key: 'btn', onClick: () => setPresenceOpen((v) => !v), style: { width: '100%', display: 'flex', alignItems: 'center', gap: '11px', padding: '7px 8px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.06)', background: 'rgba(255,255,255,0.02)', cursor: 'pointer' } }, [
h('div', { key: 'av', style: { position: 'relative', flex: 'none', width: '36px', height: '36px', borderRadius: '10px', display: 'grid', placeItems: 'center', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.24)', color: '#f0892a' } }, [
h('span', { key: 'i', style: { display: 'grid' }, dangerouslySetInnerHTML: { __html: PRES_SVG.user } }),
- h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: myMeta.dot, border: '2px solid #0c0c0e' } })
+ h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: myMeta.dot, border: '2px solid #0c0c0e' } })
]),
- h('div', { key: 'tx', style: { flex: 1, minWidth: 0, textAlign: 'left' } }, [
+ h('div', { key: 'tx', style: { flex: 1, minWidth: 0, textAlign: 'start' } }, [
h('div', { key: 'y', style: { fontSize: '13.5px', fontWeight: 700, color: '#f4f4f6' } }, t('chatHdr.you')),
h('div', { key: 'w', style: { fontSize: '12px', color: '#8a8a92' } }, myMeta.word)
]),
@@ -2865,15 +2873,15 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
])
]);
const presencePanelCollapsed = h('div', { key: 'you', style: { flex: 'none', position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '0 0 13px' } }, [
- presenceMenu({ left: '60px', bottom: '8px', width: '248px' }),
+ presenceMenu({ insetInlineStart: '60px', bottom: '8px', width: '248px' }),
h('button', { key: 'btn', onClick: () => setPresenceOpen((v) => !v), title: t('chat.yourStatusPrefix') + myMeta.word, style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'rgba(240,137,42,0.12)', border: '1px solid rgba(240,137,42,0.24)', color: '#f0892a' } }, [
h('span', { key: 'i', style: { display: 'grid' }, dangerouslySetInnerHTML: { __html: PRES_SVG.user } }),
- h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '12px', height: '12px', borderRadius: '50%', background: myMeta.dot, border: '2.5px solid #0c0c0e' } })
+ h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '12px', height: '12px', borderRadius: '50%', background: myMeta.dot, border: '2.5px solid #0c0c0e' } })
])
]);
const expandedInner = [
- h('div', { key: 'head', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 12px 0 16px', height: '64px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
+ h('div', { key: 'head', style: { flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingBlock: 0, paddingInlineStart: '16px', paddingInlineEnd: '12px', height: '64px', borderBottom: '1px solid rgba(255,255,255,0.06)' } }, [
h('div', { key: 'brand', style: { display: 'flex', alignItems: 'center', gap: '10px' } }, [brandMark(30), h('span', { key: 't', style: { fontSize: '15px', fontWeight: 800, letterSpacing: '-0.3px', color: '#f4f4f6' } }, 'SecureBit')]),
collapseBtn(SB_SVG.chevL, t('chat.collapse'))
]),
@@ -2903,7 +2911,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
onClick: () => onSelectGroup(g.id),
style: { position: 'relative', display: 'flex', alignItems: 'center', gap: '12px', padding: '11px 12px', marginBottom: '4px', borderRadius: '11px', background: g.active ? '#161618' : 'transparent', border: '1px solid ' + (g.active ? 'rgba(255,255,255,0.08)' : 'transparent'), cursor: 'pointer' }
}, [
- g.active && h('span', { key: 'bar', style: { position: 'absolute', left: 0, top: '12px', bottom: '12px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
+ g.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: 0, top: '12px', bottom: '12px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
avatar(g, 38, g.active ? '#161618' : '#0c0c0e'),
h('div', { key: 'body', style: { flex: 1, minWidth: 0 } }, [
h('div', { key: 'top', style: { display: 'flex', alignItems: 'center', gap: '7px' } }, [
@@ -2928,14 +2936,14 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
...chats.map(dockItem),
h('div', { key: 'sep', style: { width: '30px', height: '1px', background: 'rgba(255,255,255,0.07)', margin: '2px 0' } }),
...groups.map((g) => h('div', { key: g.id, style: { position: 'relative' } }, [
- g.active && h('span', { key: 'bar', style: { position: 'absolute', left: '-13px', top: '9px', bottom: '9px', width: '3px', borderRadius: '0 3px 3px 0', background: '#f0892a' } }),
+ g.active && h('span', { key: 'bar', style: { position: 'absolute', insetInlineStart: '-13px', top: '9px', bottom: '9px', width: '3px', borderStartEndRadius: '3px', borderEndEndRadius: '3px', background: '#f0892a' } }),
h('div', {
key: 'tile', onClick: () => onSelectGroup(g.id), title: g.name + ' — ' + g.headerSub,
style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: g.active ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,' + (g.active ? '0.14' : '0.07') + ')', fontSize: '13px', fontWeight: 700, letterSpacing: '-0.3px', color: g.active ? '#f4f4f6' : '#9a9aa2' }
}, [
g.mono,
- h('span', { key: 'dot', style: { position: 'absolute', right: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: g.dot, border: '2.5px solid #0c0c0e' } }),
- g.unread && h('span', { key: 'u', style: { position: 'absolute', left: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, g.unread)
+ h('span', { key: 'dot', style: { position: 'absolute', insetInlineEnd: '-2px', bottom: '-2px', width: '11px', height: '11px', borderRadius: '50%', background: g.dot, border: '2.5px solid #0c0c0e' } }),
+ g.unread && h('span', { key: 'u', style: { position: 'absolute', insetInlineStart: '-5px', top: '-5px', minWidth: '17px', height: '17px', padding: '0 4px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: '#f0892a', color: '#1a0f04', fontFamily: "'JetBrains Mono',monospace", fontSize: '9.5px', fontWeight: 700, border: '2px solid #0c0c0e' } }, g.unread)
])
])),
h('div', { key: 'gph', onClick: onNewGroup, title: t('chat.newGroup'), style: { position: 'relative', width: '44px', height: '44px', borderRadius: '12px', display: 'grid', placeItems: 'center', cursor: 'pointer', background: 'transparent', border: '1px dashed rgba(255,255,255,0.1)', color: '#56565e' }, dangerouslySetInnerHTML: { __html: SB_SVG.users } })
@@ -2948,7 +2956,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
];
const railWidth = collapsed ? '72px' : '292px';
- const railStyle = { flex: 'none', width: railWidth, display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'stretch', background: '#0c0c0e', borderRight: '1px solid rgba(255,255,255,0.06)' };
+ const railStyle = { flex: 'none', width: railWidth, display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'stretch', background: '#0c0c0e', borderInlineEnd: '1px solid rgba(255,255,255,0.06)' };
const inner = collapsed ? collapsedInner : expandedInner;
return h(React.Fragment, null, [
@@ -3030,11 +3038,11 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
onPointerUp: drawerUp,
onPointerCancel: drawerUp,
style: { position: 'fixed', inset: 0, zIndex: 60, background: 'rgba(6,6,8,0.6)', backdropFilter: 'blur(0px)', WebkitBackdropFilter: 'blur(0px)', opacity: 0, display: drawerMounted ? 'block' : 'none', touchAction: 'pan-y', willChange: 'opacity' }
- }, h('aside', { className: 'sb-mobile-drawer', ref: panelRef, onClick: (e) => e.stopPropagation(), style: { position: 'absolute', left: 0, top: 0, bottom: 0, width: 'min(292px, 86vw)', display: 'flex', flexDirection: 'column', background: '#0c0c0e', borderRight: '1px solid rgba(255,255,255,0.06)', boxShadow: '0 0 60px rgba(0,0,0,0.6)', touchAction: 'pan-y', willChange: 'transform' } }, [
+ }, h('aside', { className: 'sb-mobile-drawer', ref: panelRef, onClick: (e) => e.stopPropagation(), style: { position: 'absolute', insetInlineStart: 0, top: 0, bottom: 0, width: 'min(292px, 86vw)', display: 'flex', flexDirection: 'column', background: '#0c0c0e', borderInlineEnd: '1px solid rgba(255,255,255,0.06)', boxShadow: '0 0 60px rgba(0,0,0,0.6)', touchAction: 'pan-y', willChange: 'transform' } }, [
// Explicit close button — the drawer's own header only has a
// "collapse" chevron (a desktop-rail action), so on mobile there was
// no obvious way to dismiss it. This X closes the drawer reliably.
- h('button', { key: 'x', onClick: onCloseDrawer, title: t('chat.closeMenu'), 'aria-label': t('chat.closeMenu'), style: { position: 'absolute', top: '15px', right: '13px', zIndex: 2, width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer' } }, h('i', { className: 'fas fa-xmark', style: { fontSize: '16px' } })),
+ h('button', { key: 'x', onClick: onCloseDrawer, title: t('chat.closeMenu'), 'aria-label': t('chat.closeMenu'), style: { position: 'absolute', top: '15px', insetInlineEnd: '13px', zIndex: 2, width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)', color: '#cfcfd4', cursor: 'pointer' } }, h('i', { className: 'fas fa-xmark', style: { fontSize: '16px' } })),
expandedInner
]))
]);
@@ -6637,7 +6645,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
React.createElement('button', {
key: 'burger', className: 'sb-burger',
onClick: () => setSidebarDrawerOpen(true),
- style: { display: 'none', position: 'fixed', top: '13px', left: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
+ style: { display: 'none', position: 'fixed', top: '13px', insetInlineStart: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
dangerouslySetInnerHTML: { __html: SB_SVG.burger }
}),
React.createElement('div', {
@@ -6737,7 +6745,7 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
key: 'sb-burger',
className: 'sb-burger',
onClick: () => setSidebarDrawerOpen(true),
- style: { display: 'none', position: 'fixed', top: '13px', left: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
+ style: { display: 'none', position: 'fixed', top: '13px', insetInlineStart: '13px', zIndex: 55, width: '38px', height: '38px', borderRadius: '10px', placeItems: 'center', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(18,18,20,0.9)', color: '#cfcfd4', cursor: 'pointer' },
dangerouslySetInnerHTML: { __html: SB_SVG.burger }
}),
React.createElement('div', {
@@ -6950,10 +6958,10 @@ import { GroupChatView, GroupSasModal, CreateGroupModal, GroupInviteModal, Group
id: 'qr-reader',
style: { position: 'absolute', inset: 0, zIndex: 1 }
}),
- corner('c-tl', { top: '18px', left: '18px', borderTop: '2.5px solid #3ecf8e', borderLeft: '2.5px solid #3ecf8e', borderRadius: '8px 0 0 0' }),
- corner('c-tr', { top: '18px', right: '18px', borderTop: '2.5px solid #3ecf8e', borderRight: '2.5px solid #3ecf8e', borderRadius: '0 8px 0 0' }),
- corner('c-bl', { bottom: '18px', left: '18px', borderBottom: '2.5px solid #3ecf8e', borderLeft: '2.5px solid #3ecf8e', borderRadius: '0 0 0 8px' }),
- corner('c-br', { bottom: '18px', right: '18px', borderBottom: '2.5px solid #3ecf8e', borderRight: '2.5px solid #3ecf8e', borderRadius: '0 0 8px 0' }),
+ corner('c-tl', { top: '18px', insetInlineStart: '18px', borderTop: '2.5px solid #3ecf8e', borderInlineStart: '2.5px solid #3ecf8e', borderRadius: '8px 0 0 0' }),
+ corner('c-tr', { top: '18px', insetInlineEnd: '18px', borderTop: '2.5px solid #3ecf8e', borderInlineEnd: '2.5px solid #3ecf8e', borderRadius: '0 8px 0 0' }),
+ corner('c-bl', { bottom: '18px', insetInlineStart: '18px', borderBottom: '2.5px solid #3ecf8e', borderInlineStart: '2.5px solid #3ecf8e', borderRadius: '0 0 0 8px' }),
+ corner('c-br', { bottom: '18px', insetInlineEnd: '18px', borderBottom: '2.5px solid #3ecf8e', borderInlineEnd: '2.5px solid #3ecf8e', borderRadius: '0 0 8px 0' }),
React.createElement('span', {
key: 'scan-line',
style: { position: 'absolute', left: '18px', right: '18px', height: '2.5px', zIndex: 2, background: 'linear-gradient(90deg, transparent, #3ecf8e, transparent)', boxShadow: '0 0 16px #3ecf8e', animation: 'sbScan 1.5s ease-in-out infinite alternate' }
diff --git a/src/components/QRScanner.js b/src/components/QRScanner.js
index 62e6208..ca449bc 100644
--- a/src/components/QRScanner.js
+++ b/src/components/QRScanner.js
@@ -461,7 +461,7 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
}, [
React.createElement('i', {
key: 'tips-icon',
- className: 'fas fa-lightbulb mr-2'
+ className: 'fas fa-lightbulb me-2'
}),
'Tips for better scanning:'
]),
diff --git a/src/components/UpdateChecker.jsx b/src/components/UpdateChecker.jsx
index 1ad3923..86068a5 100644
--- a/src/components/UpdateChecker.jsx
+++ b/src/components/UpdateChecker.jsx
@@ -183,7 +183,7 @@ const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
// version comparison
React.createElement('div', {
key: 'vbox',
- style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'left' }
+ style: { borderRadius: '14px', background: '#0c0c0e', border: '1px solid rgba(255,255,255,0.06)', padding: '16px 18px', marginBottom: '24px', textAlign: 'start' }
}, [
React.createElement('div', { key: 'cur', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '14px', padding: '5px 0' } }, [
React.createElement('span', { key: 'l', style: { fontSize: '13.5px', fontWeight: 500, color: '#8a8a92' } }, t('update.currentVersion')),
diff --git a/src/components/ui/BecomePartner.jsx b/src/components/ui/BecomePartner.jsx
index aad1773..fd90b0c 100644
--- a/src/components/ui/BecomePartner.jsx
+++ b/src/components/ui/BecomePartner.jsx
@@ -47,9 +47,9 @@ const BecomePartner = () => {
}
];
- const svg = (inner, size, stroke, sw) =>
+ const svg = (inner, size, stroke, sw, className) =>
React.createElement('svg', {
- width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
+ className, width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
stroke, strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round',
dangerouslySetInnerHTML: { __html: inner }
});
@@ -123,7 +123,7 @@ const BecomePartner = () => {
}
}, [
t('partners.inviteCta'),
- svg('
', 17, 'currentColor', 2.2)
+ svg('
', 17, 'currentColor', 2.2, 'sb-mirror-rtl')
])
]);
diff --git a/src/components/ui/CallUI.jsx b/src/components/ui/CallUI.jsx
index f0fc430..f172812 100644
--- a/src/components/ui/CallUI.jsx
+++ b/src/components/ui/CallUI.jsx
@@ -177,12 +177,12 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
// ── MINIMIZED widget ─────────────────────────────────────────────────────
if (minimized) {
- return h('div', { style: { position: 'absolute', bottom: '18px', right: '18px', zIndex: 40, width: '236px', borderRadius: '14px', overflow: 'hidden', background: '#161618', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)', animation: 'sbExpand .18s ease' } }, [
+ return h('div', { style: { position: 'absolute', bottom: '18px', insetInlineEnd: '18px', zIndex: 40, width: '236px', borderRadius: '14px', overflow: 'hidden', background: '#161618', border: '1px solid rgba(255,255,255,0.1)', boxShadow: '0 18px 44px rgba(0,0,0,0.55)', animation: 'sbExpand .18s ease' } }, [
hiddenAudio,
isVideo && h('div', { key: 'v', style: { position: 'relative', height: '132px', background: '#111' } }, [
h('video', { key: 'rv', ref: remoteVideoRef, autoPlay: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } }),
!call.remoteHasVideo && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', background: 'linear-gradient(120deg,#15151b,#1d1a24)', color: '#6b6b73' } }, svg(ICON.camOff, 22, 1.8)),
- h('span', { key: 's', style: { position: 'absolute', top: '8px', left: '9px', fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#fff', padding: '3px 7px', borderRadius: '6px', background: 'rgba(0,0,0,0.5)' } }, callStatus)
+ h('span', { key: 's', style: { position: 'absolute', top: '8px', insetInlineStart: '9px', fontFamily: MONO, fontSize: '11px', fontWeight: 600, color: '#fff', padding: '3px 7px', borderRadius: '6px', background: 'rgba(0,0,0,0.5)' } }, callStatus)
]),
h('div', { key: 'bar', style: { display: 'flex', alignItems: 'center', gap: '11px', padding: '11px 12px' } }, [
h('span', { key: 'ic', style: { position: 'relative', flex: 'none', width: '34px', height: '34px', borderRadius: '9px', display: 'grid', placeItems: 'center', background: 'rgba(62,207,142,0.1)', border: '1px solid rgba(62,207,142,0.25)', color: '#3ecf8e' } }, svg(ICON.user, 16, 1.9)),
@@ -222,7 +222,7 @@ const CallUIComponent = ({ webrtcManager, peerTitle }) => {
h('button', { key: 'min', onClick: () => setMinimized(true), title: t('call.minimize'), style: { flex: 'none', ...minimizeBtn(true) } }, svg(ICON.minimize, 16, 2))
]),
// Self-cam PiP
- h('div', { key: 'self', style: { position: 'absolute', bottom: '108px', right: '18px', width: '132px', height: '176px', borderRadius: '14px', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.16)', boxShadow: '0 12px 30px rgba(0,0,0,0.5)', background: '#111' } }, [
+ h('div', { key: 'self', style: { position: 'absolute', bottom: '108px', insetInlineEnd: '18px', width: '132px', height: '176px', borderRadius: '14px', overflow: 'hidden', border: '1px solid rgba(255,255,255,0.16)', boxShadow: '0 12px 30px rgba(0,0,0,0.5)', background: '#111' } }, [
h('video', { key: 'sv', ref: selfVideoRef, autoPlay: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover', transform: 'scaleX(-1)', display: 'block' } }),
!call.cameraEnabled && h('div', { key: 'off', style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', background: '#161618', color: '#6b6b73' } }, [
svg(ICON.camOff, 24, 1.8),
diff --git a/src/components/ui/ComparisonTable.jsx b/src/components/ui/ComparisonTable.jsx
index 60fb990..5db8969 100644
--- a/src/components/ui/ComparisonTable.jsx
+++ b/src/components/ui/ComparisonTable.jsx
@@ -180,7 +180,7 @@
{/* Mobile Alert */}
-
+
Rotate your device horizontally for better viewing
@@ -194,7 +194,7 @@
{/* Table Header */}
-
+
Security Criterion
{messengers.map((messenger, index) => (
@@ -282,20 +282,20 @@
{/* Legend */}
-
+
Category Leader
-
+
Excellent
-
+
Partial/Limited
-
+
Not Available
diff --git a/src/components/ui/FileTransfer.jsx b/src/components/ui/FileTransfer.jsx
index 74ad2ec..7a5d139 100644
--- a/src/components/ui/FileTransfer.jsx
+++ b/src/components/ui/FileTransfer.jsx
@@ -203,7 +203,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}, [
React.createElement('i', {
key: 'icon',
- className: 'fas fa-exclamation-triangle mr-2'
+ className: 'fas fa-exclamation-triangle me-2'
}),
'Соединение устанавливается... Передача файлов будет доступна после завершения установки.'
]);
@@ -333,7 +333,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
React.createElement('i', {
key: 'icon',
className: 'fas fa-arrow-up',
- style: { color: '#f0892a', fontSize: '13px', marginRight: '8px' }
+ style: { color: '#f0892a', fontSize: '13px', marginInlineEnd: '8px' }
}),
React.createElement('span', {
key: 'name',
@@ -342,7 +342,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}, transfer.fileName),
React.createElement('span', {
key: 'size',
- className: "text-xs ml-2",
+ className: "text-xs ms-2",
style: { color: '#7b7b83' }
}, formatFileSize(transfer.fileSize))
]),
@@ -377,7 +377,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
React.createElement('i', {
key: 'icon',
className: 'fas fa-arrow-down',
- style: { color: '#3ecf8e', fontSize: '13px', marginRight: '8px' }
+ style: { color: '#3ecf8e', fontSize: '13px', marginInlineEnd: '8px' }
}),
React.createElement('span', {
key: 'name',
@@ -386,7 +386,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}, transfer.fileName),
React.createElement('span', {
key: 'size',
- className: "text-xs ml-2",
+ className: "text-xs ms-2",
style: { color: '#7b7b83' }
}, formatFileSize(transfer.fileSize))
]),
@@ -408,7 +408,7 @@ const FileTransferComponent = ({ webrtcManager, isConnected, pendingIncomingFile
}
}
}, [
- React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
+ React.createElement('i', { key: 'i', className: 'fas fa-download me-1' }),
t('file.download')
]) : null,
React.createElement('button', {
diff --git a/src/components/ui/GroupChat.jsx b/src/components/ui/GroupChat.jsx
index 7513c23..64c47fc 100644
--- a/src/components/ui/GroupChat.jsx
+++ b/src/components/ui/GroupChat.jsx
@@ -167,9 +167,12 @@ export function GroupSasModal({ group, onConfirm, onCancel }) {
background: 'rgba(240,137,42,0.09)', border: '1px solid rgba(240,137,42,0.3)',
},
}, h('span', {
+ // Everyone in the group compares these digits against each other's
+ // screens, so their order must not depend on the reader's language.
+ dir: 'ltr',
style: {
fontFamily: C.mono, fontSize: 'clamp(30px, 9vw, 42px)', fontWeight: 700,
- letterSpacing: '9px', color: C.accent,
+ letterSpacing: '9px', color: C.accent, unicodeBidi: 'isolate',
},
}, group.sasCode)),
@@ -277,7 +280,7 @@ export function CreateGroupModal({ candidates, relayOnly, onCreate, onCancel })
disabled: full,
style: {
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
- borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'left',
+ borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'start',
background: on ? 'rgba(240,137,42,0.1)' : 'transparent',
border: `1px solid ${on ? 'rgba(240,137,42,0.32)' : C.line}`,
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
@@ -405,7 +408,7 @@ export function AddMembersModal({ candidates, remaining, onAdd, onCancel }) {
disabled: full,
style: {
display: 'flex', alignItems: 'center', gap: '11px', padding: '10px 12px',
- borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'left',
+ borderRadius: '10px', cursor: full ? 'not-allowed' : 'pointer', textAlign: 'start',
background: on ? 'rgba(240,137,42,0.1)' : 'transparent',
border: `1px solid ${on ? 'rgba(240,137,42,0.32)' : C.line}`,
opacity: full ? 0.4 : 1, fontFamily: 'inherit',
@@ -558,7 +561,7 @@ function Bubble({ msg }) {
}, [
!mine && h('span', {
key: 'who',
- style: { fontSize: '11.5px', fontWeight: 600, color: C.accent, paddingLeft: '3px' },
+ style: { fontSize: '11.5px', fontWeight: 600, color: C.accent, paddingInlineStart: '3px' },
}, msg.senderName || t('group.member')),
h('div', {
key: 'b',
@@ -626,7 +629,9 @@ export function GroupChatView({
}, [
svg(ICON.users, { key: 'i', width: '13px', height: '13px' }),
t('group.membersCount', { count: group.members.length }),
- ready && group.sasCode ? t('group.codeSuffix', { code: group.sasCode }) : '',
+ ready && group.sasCode
+ ? h('span', { key: 'code', dir: 'ltr', style: { unicodeBidi: 'isolate' } }, t('group.codeSuffix', { code: group.sasCode }))
+ : '',
]),
]),
(isAdmin && onAddMembers && group.members.length < GROUP_LIMITS.MAX_MEMBERS) && h('button', {
diff --git a/src/components/ui/IceServerSettings.jsx b/src/components/ui/IceServerSettings.jsx
index 04bcf9c..a804dd0 100644
--- a/src/components/ui/IceServerSettings.jsx
+++ b/src/components/ui/IceServerSettings.jsx
@@ -1,4 +1,4 @@
-import { t } from '../../i18n/index.js';
+import { t, direction } from '../../i18n/index.js';
// Advanced network settings: lets a user supply their own STUN/TURN servers
// instead of the bundled public defaults, and toggle relay-only privacy mode.
// Free / power-user feature, hidden behind an explicit "Advanced" entry point.
@@ -115,7 +115,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const radioCard = (selected, onClick, title, desc, extraStyle) => h('button', {
type: 'button', onClick,
style: Object.assign({
- width: '100%', textAlign: 'left', display: 'flex', alignItems: 'flex-start', gap: '12px',
+ width: '100%', textAlign: 'start', display: 'flex', alignItems: 'flex-start', gap: '12px',
padding: '14px 15px', borderRadius: '13px',
border: `1px solid ${selected ? 'rgba(240,137,42,0.45)' : 'rgba(255,255,255,0.07)'}`,
background: selected ? 'rgba(240,137,42,0.06)' : '#141416',
@@ -134,7 +134,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const toggleRow = (on, onClick, title, desc, accent, badge) => h('button', {
type: 'button', onClick,
style: {
- width: '100%', textAlign: 'left', display: 'flex', alignItems: 'flex-start', gap: '12px',
+ width: '100%', textAlign: 'start', display: 'flex', alignItems: 'flex-start', gap: '12px',
padding: '14px 15px', borderRadius: '13px',
border: `1px solid ${on ? 'rgba(62,207,142,0.3)' : 'rgba(255,255,255,0.07)'}`,
background: on ? 'rgba(62,207,142,0.05)' : '#141416',
@@ -149,7 +149,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
h('span', { key: 'd', style: { display: 'block', fontSize: '12.5px', lineHeight: 1.5, color: '#8a8a92', marginTop: '3px' } }, desc)
]),
h('span', { key: 'tr', style: { flex: 'none', width: '42px', height: '24px', borderRadius: '99px', background: on ? (accent || C_GREEN) : 'rgba(255,255,255,0.08)', border: `1px solid ${on ? (accent || C_GREEN) : 'rgba(255,255,255,0.12)'}`, position: 'relative', transition: 'all .18s', marginTop: '1px' } },
- h('span', { style: { position: 'absolute', top: '2px', left: '2px', width: '18px', height: '18px', borderRadius: '50%', background: '#fff', transform: on ? 'translateX(18px)' : 'translateX(0)', transition: 'transform .18s' } }))
+ h('span', { style: { position: 'absolute', top: '2px', insetInlineStart: '2px', width: '18px', height: '18px', borderRadius: '50%', background: '#fff', transform: on ? `translateX(${18 * direction()}px)` : 'translateX(0)', transition: 'transform .18s' } }))
]);
// ── scrollable body ──
@@ -163,16 +163,17 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const custom = [];
custom.push(h('div', { key: 'ta', style: { borderRadius: '13px', border: '1px solid rgba(255,255,255,0.08)', background: '#0c0c0e', overflow: 'hidden', marginBottom: '12px' } },
h('textarea', {
+ dir: 'ltr',
value: serversText, onChange: (e) => setServersText(e.target.value), rows: 5, spellCheck: false, autoComplete: 'off',
placeholder: PLACEHOLDER,
style: { width: '100%', resize: 'vertical', border: 'none', outline: 'none', background: 'transparent', color: '#c9ccd8', fontFamily: MONO, fontSize: '12px', lineHeight: 1.65, padding: '13px 14px', minHeight: '104px' }
})));
if (parsed.errors.length > 0) {
- custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingLeft: '18px', color: '#e5727a', fontSize: '12.5px' } },
+ custom.push(h('ul', { key: 'err', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e5727a', fontSize: '12.5px' } },
parsed.errors.slice(0, 6).map((err, i) => h('li', { key: i }, err))));
}
if (parsed.warnings.length > 0) {
- custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingLeft: '18px', color: '#e3c84e', fontSize: '12.5px' } },
+ custom.push(h('ul', { key: 'warn', style: { margin: '0 0 10px', paddingInlineStart: '18px', color: '#e3c84e', fontSize: '12.5px' } },
parsed.warnings.slice(0, 6).map((w, i) => h('li', { key: i }, w))));
}
if (parsed.servers.length > 0 && parsed.errors.length === 0) {
@@ -219,7 +220,7 @@ const IceServerSettings = ({ isOpen, onClose, initial, hasSaved, onApply, onForg
const footerBtns = [];
if (hasSaved) {
footerBtns.push(h('button', { key: 'forget', type: 'button', onClick: handleForget,
- style: { marginRight: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(229,114,122,0.3)', background: 'transparent', color: '#e5727a', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.forget')));
+ style: { marginInlineEnd: 'auto', padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(229,114,122,0.3)', background: 'transparent', color: '#e5727a', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.forget')));
}
footerBtns.push(h('button', { key: 'cancel', type: 'button', onClick: onClose,
style: { padding: '11px 18px', borderRadius: '11px', border: '1px solid rgba(255,255,255,0.1)', background: 'transparent', color: '#b3b3ba', fontFamily: 'inherit', fontSize: '13.5px', fontWeight: 600, cursor: 'pointer' } }, t('ice.cancel')));
diff --git a/src/components/ui/LanguageSwitcher.jsx b/src/components/ui/LanguageSwitcher.jsx
index 2218b38..c2d15f8 100644
--- a/src/components/ui/LanguageSwitcher.jsx
+++ b/src/components/ui/LanguageSwitcher.jsx
@@ -68,20 +68,30 @@ const LanguageSwitcher = () => {
const menu = React.createElement('div', {
key: 'menu',
role: 'menu',
+ // The same scrollbar the chat uses, rather than the browser's default slab —
+ // a 170px menu is exactly where a 15px stock scrollbar looks like a mistake.
+ className: 'sb-scroll',
// Kept in the DOM when shut: the links stay followable and the menu costs nothing
// to reopen.
style: {
- position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 60,
+ position: 'absolute', top: 'calc(100% + 6px)', insetInlineEnd: 0, zIndex: 60,
display: open ? 'block' : 'none',
minWidth: '170px', padding: '5px', borderRadius: '11px',
border: '1px solid rgba(255,255,255,0.08)', background: '#161618',
boxShadow: '0 14px 34px rgba(0,0,0,0.45)',
+ // Thirteen languages is taller than a phone in landscape. Cap it and scroll
+ // rather than let the list run off the bottom, where the last few entries
+ // would be unreachable.
+ maxHeight: 'min(62vh, 420px)', overflowY: 'auto', overscrollBehavior: 'contain',
},
}, links.map((link) => React.createElement('a', {
key: link.code,
href: link.href,
hrefLang: link.hrefLang,
lang: link.hrefLang,
+ // Each row is a word in its own script, so it gets its own direction — the menu
+ // around it stays in the direction of the page being read.
+ dir: link.dir,
role: 'menuitem',
// aria-current names the page you are on for a screen reader; the weight and
// contrast below say the same thing to everyone else.
@@ -101,7 +111,7 @@ const LanguageSwitcher = () => {
}, [
React.createElement('span', {
key: 'a',
- style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: '#6b6b73', width: '22px' },
+ style: { fontSize: '11px', fontWeight: 700, letterSpacing: '0.4px', color: '#6b6b73', width: '22px', flex: 'none', textAlign: 'start' },
}, link.abbr),
React.createElement('span', { key: 'n' }, link.label),
])));
diff --git a/src/components/ui/PasswordModal.jsx b/src/components/ui/PasswordModal.jsx
index b3b7c7f..2d88cd9 100644
--- a/src/components/ui/PasswordModal.jsx
+++ b/src/components/ui/PasswordModal.jsx
@@ -29,7 +29,7 @@ const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPasswor
}, [
React.createElement('div', {
key: 'icon',
- className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
+ className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center me-3'
}, [
React.createElement('i', {
className: 'fas fa-key accent-purple'
@@ -68,7 +68,7 @@ const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPasswor
className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
}, [
React.createElement('i', {
- className: 'fas fa-unlock-alt mr-2'
+ className: 'fas fa-unlock-alt me-2'
}),
t('pw.decrypt')
]),
@@ -79,7 +79,7 @@ const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPasswor
className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
}, [
React.createElement('i', {
- className: 'fas fa-times mr-2'
+ className: 'fas fa-times me-2'
}),
t('pw.cancel')
])
diff --git a/src/components/ui/Roadmap.jsx b/src/components/ui/Roadmap.jsx
index 9825b42..d2f3778 100644
--- a/src/components/ui/Roadmap.jsx
+++ b/src/components/ui/Roadmap.jsx
@@ -75,28 +75,28 @@ function Roadmap() {
const renderNode = (status) => {
if (status === 'released') {
return (
-
+
);
}
if (status === 'current') {
return (
-
+
);
}
if (status === 'dev') {
return (
-
+
);
}
// planned / research
return (
-
+
);
@@ -134,7 +134,7 @@ function Roadmap() {
{/* spine */}
- {notLast &&
}
+ {notLast &&
}
{renderNode(d.status)}
@@ -156,7 +156,7 @@ function Roadmap() {
{!isMobile && meta.word}
- {!isMobile &&
{d.date} }
+ {!isMobile &&
{d.date} }
diff --git a/src/components/ui/Testimonials.jsx b/src/components/ui/Testimonials.jsx
index 990f11f..110803a 100644
--- a/src/components/ui/Testimonials.jsx
+++ b/src/components/ui/Testimonials.jsx
@@ -84,7 +84,7 @@ const Testimonials = () => {
{"★".repeat(Math.floor(t.rating))}
- {t.rating.toFixed(1)}
+ {t.rating.toFixed(1)}
{t.text}
@@ -104,8 +104,8 @@ const Testimonials = () => {
-
-
+
+
{testimonials.map((t, i) => renderCard(t, i))}
diff --git a/src/components/ui/UniqueFeatureSlider.jsx b/src/components/ui/UniqueFeatureSlider.jsx
index a622a9f..5ae22a5 100644
--- a/src/components/ui/UniqueFeatureSlider.jsx
+++ b/src/components/ui/UniqueFeatureSlider.jsx
@@ -82,7 +82,7 @@ const UniqueFeatureSlider = () => {
const navBtn = (key, onClick, path) =>
React.createElement('button', {
- key, onClick, 'aria-label': key,
+ key, onClick, 'aria-label': key, className: 'sb-mirror-rtl',
style: {
width: '46px', height: '46px', display: 'grid', placeItems: 'center',
borderRadius: '50%', border: '1px solid rgba(255,255,255,0.1)',
diff --git a/src/i18n/generated.js b/src/i18n/generated.js
index fcff738..71f13ab 100644
--- a/src/i18n/generated.js
+++ b/src/i18n/generated.js
@@ -2,7 +2,7 @@
// Add or change strings in locales/
.json, then run `npm run build:i18n`.
export const DEFAULT_LOCALE = "en";
-export const SUPPORTED_LOCALES = ["en","de","fr","es","uk","ru","zh","ko","hi"];
+export const SUPPORTED_LOCALES = ["en","de","fr","es","uk","ru","zh","ko","hi","ar","he","fa","ur"];
export const LOCALE_META = {
"en": {
"htmlLang": "en",
@@ -66,6 +66,34 @@ export const LOCALE_META = {
"abbr": "HI",
"dir": "ltr",
"path": "/hi/"
+ },
+ "ar": {
+ "htmlLang": "ar",
+ "nativeName": "العربية",
+ "abbr": "AR",
+ "dir": "rtl",
+ "path": "/ar/"
+ },
+ "he": {
+ "htmlLang": "he",
+ "nativeName": "עברית",
+ "abbr": "HE",
+ "dir": "rtl",
+ "path": "/he/"
+ },
+ "fa": {
+ "htmlLang": "fa",
+ "nativeName": "فارسی",
+ "abbr": "FA",
+ "dir": "rtl",
+ "path": "/fa/"
+ },
+ "ur": {
+ "htmlLang": "ur",
+ "nativeName": "اردو",
+ "abbr": "UR",
+ "dir": "rtl",
+ "path": "/ur/"
}
};
export const DICTIONARIES = {
@@ -6809,5 +6837,3001 @@ export const DICTIONARIES = {
"offline.learnMore": "और जानें",
"pwa.installApp": "ऐप इंस्टॉल करें",
"chat.onWeb": "आप वेब संस्करण पर हैं"
+ },
+ "ar": {
+ "language.label": "اللغة",
+ "community.title": "انضمّ إلى مستقبل الخصوصية",
+ "community.description": "ينمو SecureBit بفضل مجتمعه. أفكاركم وملاحظاتكم هي ما يصوغ مستقبل التواصل الآمن - مبنيّ في العلن، مع تحقّق ASN.1 كامل من طرف إلى طرف.",
+ "community.github": "مستودع GitHub",
+ "community.feedback": "الملاحظات",
+ "unique.eyebrow": "ما الذي يميّزنا",
+ "unique.heading": "لماذا SecureBit فريد",
+ "unique.s1.titleTop": "نواة تشفير",
+ "unique.s1.titleBottom": "متعدّدة الطبقات",
+ "unique.s1.collapsed": "نواة التشفير",
+ "unique.s1.desc": "تبادل مفاتيح ECDH P-384، وحمولات AES-256-GCM، وتواقيع ECDSA، وتحقّق ASN.1 كامل - مجموعة في خطّ واحد مُحصَّن.",
+ "unique.s1.tags": [
+ "ECDH P-384",
+ "AES-256-GCM",
+ "ECDSA",
+ "ASN.1"
+ ],
+ "unique.s2.titleTop": "ندٌّ لندّ خالص",
+ "unique.s2.titleBottom": "عبر WebRTC",
+ "unique.s2.collapsed": "WebRTC ندٌّ لندّ خالص",
+ "unique.s2.desc": "تنتقل الرسائل مباشرةً بين الأجهزة عبر WebRTC. لا مُرحِّل يحتفظ ببياناتك - الخادم يساعد الطرفين على إيجاد بعضهما فقط.",
+ "unique.s2.tags": [
+ "DTLS 1.3",
+ "بلا مُرحِّل"
+ ],
+ "unique.s3.titleTop": "سرّية تامّة",
+ "unique.s3.titleBottom": "إلى الأمام",
+ "unique.s3.collapsed": "السرّية التامّة للأمام",
+ "unique.s3.desc": "تتبدّل مفاتيح الجلسة باستمرار وتُتلَف بعد الاستعمال، فلا يفتح مفتاح واحد مسروق أيّ محادثة ماضية.",
+ "unique.s3.tags": [
+ "مفاتيح عابرة",
+ "تبديل تلقائي"
+ ],
+ "unique.s4.titleTop": "تمويه",
+ "unique.s4.titleBottom": "حركة البيانات",
+ "unique.s4.collapsed": "تمويه حركة البيانات",
+ "unique.s4.desc": "تُحشى أحجام الحزم وتوقيتاتها وتُعشوَأ، فتختفي أنماط البيانات الوصفية عن أعين من يراقب الخطّ.",
+ "unique.s4.tags": [
+ "حشو الحزم",
+ "تشويش التوقيت"
+ ],
+ "unique.s5.titleTop": "صفر",
+ "unique.s5.titleBottom": "جمعٍ للبيانات",
+ "unique.s5.collapsed": "صفر جمعٍ للبيانات",
+ "unique.s5.desc": "لا حسابات ولا سجلّات ولا تخزين للرسائل. لا شيء على خادم يمكن أن يتسرّب أو يُطلَب قضائياً أو يُباع.",
+ "unique.s5.tags": [
+ "بلا حسابات",
+ "بلا سجلّات"
+ ],
+ "partners.eyebrow": "الشركاء والمنظومة",
+ "partners.heading": "شركاؤنا يثقون بنا",
+ "partners.aegis.desc": "شريك رأسمالي يؤمّن الاتصالات المالية السرّية عبر محفظته الاستثمارية.",
+ "partners.aegis.role": "داعم استراتيجي",
+ "partners.furilabs.desc": "هواتف لينكس تضع الخصوصية أولاً وتشحن SecureBit كقناة آمنة افتراضية.",
+ "partners.furilabs.role": "شريك تقني",
+ "partners.inviteTitle": "كن شريكاً",
+ "partners.inviteDesc": "هل تبني عتاداً أو بنية تحتية للخصوصية؟ لنُدمج SecureBit معاً.",
+ "partners.inviteCta": "ابدأ محادثة",
+ "roadmap.eyebrow": "خارطة طريق التطوير",
+ "roadmap.heading": "تطوّر SecureBit",
+ "roadmap.subheading": "من أوّل نموذج أوّلي إلى شبكة لامركزية مقاومة للحوسبة الكمّية - مع تحقّق ASN.1 كامل في كلّ طبقة.",
+ "roadmap.progress": "أُنجزت {shipped} من أصل {total} محطّة",
+ "roadmap.upcoming": "{upcoming} في الطريق",
+ "roadmap.keyFeatures": "أبرز المزايا",
+ "roadmap.status.released": "صدرت",
+ "roadmap.status.current": "الحالية",
+ "roadmap.status.dev": "قيد التطوير",
+ "roadmap.status.planned": "مُخطّطة",
+ "roadmap.status.research": "قيد البحث",
+ "hero.headlineTop": "خطٌّ مباشر",
+ "hero.headlineBottom": "لا يقرأه إلّا أنتما.",
+ "hero.subheading": "تُولَّد المفاتيح على جهازك وتُتبادَل ندّاً لندّ. بلا حسابات وبلا خوادم تحفظ رسائلك.",
+ "hero.diagramAlt": "خطّ مشفّر مباشر إلى طرف واحد، مع انضمام طرفين آخرين إلى الشبكة",
+ "intro.createTitle": "أنشئ قناة جديدة",
+ "intro.createDesc": "يولّد جهازك المفاتيح ودعوة تُستعمل مرّة واحدة. لا شيء يمرّ عبر خادم.",
+ "intro.createCta": "توليد المفاتيح والدعوة",
+ "intro.joinTitle": "انضمّ إلى قناة",
+ "intro.scanTitle": "امسح رمز QR بالكاميرا",
+ "intro.scanSubtitle": "الأسرع - وجّهها إلى شاشة الطرف الآخر",
+ "intro.orPasteCode": "أو ألصق الرمز",
+ "intro.pastePlaceholder": "ألصق رمز الدعوة هنا…",
+ "intro.connect": "اتّصل",
+ "intro.connecting": "جارٍ المعالجة…",
+ "roadmap.r1.title": "بداية التطوير",
+ "roadmap.r1.sub": "الفكرة والنموذج الأوّلي وتهيئة البنية التحتية",
+ "roadmap.r1.date": "أوائل 2025",
+ "roadmap.r1.features": [
+ "بلورة الفكرة والمتطلّبات",
+ "اختيار التقنيات: WebRTC وP2P والتعمية",
+ "أوّل نماذج للمراسلة",
+ "إنشاء المستودع والتكامل المستمر",
+ "بنية التشفير الأساسية",
+ "تصميم تجربة وواجهة الاستخدام"
+ ],
+ "roadmap.r2.title": "إصدار ألفا",
+ "roadmap.r2.sub": "أوّل ألفا علنية: دردشة أساسية وتبادل مفاتيح",
+ "roadmap.r2.date": "ربيع 2025",
+ "roadmap.r2.features": [
+ "مراسلة ندٌّ لندّ أساسية عبر WebRTC",
+ "تشفير بسيط من طرف إلى طرف (مخطّط تجريبي)",
+ "إشارات مستقرّة وإعادة اتصال",
+ "واجهة بحدّها الأدنى للاختبار",
+ "جمع ملاحظات المختبِرين الأوائل"
+ ],
+ "roadmap.r3.title": "تحصين أمني",
+ "roadmap.r3.sub": "تعزيز الأمان وإطلاق فرع مستقرّ",
+ "roadmap.r3.date": "صيف 2025",
+ "roadmap.r3.features": [
+ "تطبيق ECDH/ECDSA في الإنتاج",
+ "السرّية التامّة للأمام وتبديل المفاتيح",
+ "تحسين فحوص الاستيثاق",
+ "تشفير الملفّات ونقل الحمولات الكبيرة",
+ "تدقيق عمليات التعمية الأساسية"
+ ],
+ "roadmap.r4.title": "التوسّع والاستقرار",
+ "roadmap.r4.sub": "توسيع الشبكة وتحسين الاستقرار",
+ "roadmap.r4.date": "خريف 2025",
+ "roadmap.r4.features": [
+ "تحسين اتصالات P2P واجتياز NAT",
+ "آليات إعادة الاتصال وطوابير الرسائل",
+ "خفض استهلاك البطارية على الهواتف",
+ "دعم المزامنة بين عدّة أجهزة",
+ "أدوات مراقبة وتسجيل للمطوّرين"
+ ],
+ "roadmap.r5.title": "إصدار الخصوصية أوّلاً",
+ "roadmap.r5.sub": "التركيز على الخصوصية: تقليل البيانات الوصفية",
+ "roadmap.r5.date": "شتاء 2025",
+ "roadmap.r5.features": [
+ "حماية البيانات الوصفية وتقليل البصمة",
+ "تجارب على توجيه البصلة وجداول DHT",
+ "خيارات للاتصالات المجهولة",
+ "التحضير لتدقيق مفتوح للشيفرة",
+ "تحسين عمليات التحقّق من المستخدمين"
+ ],
+ "roadmap.r6.title": "إصدار الأمان المعزَّز",
+ "roadmap.r6.sub": "تعمية بمستوى عسكري من 18 طبقة مع تحقّق ASN.1 كامل",
+ "roadmap.r6.date": "أواخر 2025",
+ "roadmap.r6.features": [
+ "أمان ثلاثي الطبقات: ECDH وDTLS وSAS",
+ "تشفير ECDH P-384 مع AES-GCM بطول 256 بت",
+ "التحقّق من بصمة DTLS",
+ "تحقّق SAS (سلسلة استيثاق قصيرة)",
+ "السرّية التامّة للأمام مع تبديل المفاتيح",
+ "حماية مُعزَّزة من هجمات الوسيط",
+ "تحقّق ASN.1 DER كامل",
+ "التحقّق من المعرّفات OID ونقاط المنحنى",
+ "التحقّق من بنية SPKI",
+ "بنية WebRTC ندٌّ لندّ",
+ "حماية البيانات الوصفية",
+ "شيفرة مفتوحة المصدر 100%"
+ ],
+ "roadmap.r7.title": "إصدار سطح المكتب",
+ "roadmap.r7.sub": "تطبيقات أصلية لويندوز وماك ولينكس",
+ "roadmap.r7.date": "أوائل 2026",
+ "roadmap.r7.features": [
+ "تطبيق ويندوز (Tauri v2)",
+ "تطبيق macOS (Tauri v2)",
+ "دعم AppImage للينكس (Tauri v2)",
+ "إشعارات فورية",
+ "إعادة اتصال تلقائية",
+ "مزامنة بين الأجهزة",
+ "تحسين تجربة وواجهة الاستخدام",
+ "دعم ملفّات حتى 100 ميغابايت"
+ ],
+ "roadmap.r8.title": "الصوت والمكالمات الآمنة",
+ "roadmap.r8.sub": "رسائل صوتية ومكالمات صوتية ومرئية مشفّرة",
+ "roadmap.r8.date": "أوائل 2026",
+ "roadmap.r8.features": [
+ "رسائل صوتية مشفّرة من طرف إلى طرف",
+ "مكالمات صوتية مشفّرة ثنائية (WebRTC)",
+ "مكالمات مرئية مشفّرة ثنائية (WebRTC)",
+ "سرّية تامّة للأمام للوسائط الحيّة",
+ "تدفّقات وسائط محميّة بـ SRTP/DTLS",
+ "تحقّق SAS أثناء المكالمة",
+ "إشعارات المكالمات وإعادة الاتصال التلقائية",
+ "وسائط ندٌّ لندّ بزمن استجابة منخفض"
+ ],
+ "roadmap.r9.title": "المحادثات الجماعية",
+ "roadmap.r9.sub": "دردشات جماعية مع الحفاظ على الخصوصية",
+ "roadmap.r9.date": "الآن",
+ "roadmap.r9.features": [
+ "دردشات جماعية ندٌّ لندّ حتى 8 مشاركين",
+ "توصيل شبكي مع احتياطي ترحيل موقَّع",
+ "رمز أمان واحد للمجموعة يقارنه الجميع",
+ "مراسم الالتزام ثمّ الكشف ضدّ تخمين الرمز",
+ "مفاتيح هوية لكلّ مجموعة، عابرة بالتصميم",
+ "عضوية موقَّعة مع ترتيب حِقَبي",
+ "رسائل موقَّعة، فأيّ نصّ منقسم يمكن إثباته",
+ "بلا خادم وبلا مفتاح جماعي مشترك وبلا سجلّ"
+ ],
+ "roadmap.r10.title": "إصدار الهواتف",
+ "roadmap.r10.sub": "تطبيقات أصلية لـ iOS وأندرويد",
+ "roadmap.r10.date": "الربع الثاني 2027",
+ "roadmap.r10.features": [
+ "تطبيق iOS أصلي (Swift/SwiftUI)",
+ "تطبيق أندرويد أصلي (Kotlin/Jetpack Compose)",
+ "دعم PWA لمتصفّحات الهواتف",
+ "إشعارات فورية مدفوعة",
+ "تحسين استهلاك البطارية",
+ "واجهة مهيّأة للهواتف",
+ "طابور رسائل دون اتصال",
+ "استيثاق حيوي"
+ ],
+ "roadmap.r11.title": "إصدار مقاوم للكمّ",
+ "roadmap.r11.sub": "حماية من الحواسيب الكمّية",
+ "roadmap.r11.date": "الربع الرابع 2027",
+ "roadmap.r11.features": [
+ "تعمية ما بعد الكمّ CRYSTALS-Kyber",
+ "تواقيع رقمية SPHINCS+",
+ "مخطّط هجين: كلاسيكي + ما بعد الكمّ",
+ "تبادل مفاتيح آمن كمّياً",
+ "خوارزميات تجزئة محدَّثة",
+ "ترحيل الجلسات القائمة",
+ "توافق مع الإصدار 5.x",
+ "بروتوكولات مقاومة للكمّ"
+ ],
+ "roadmap.r12.title": "شبكة لامركزية",
+ "roadmap.r12.sub": "شبكة لامركزية بالكامل",
+ "roadmap.r12.date": "2028",
+ "roadmap.r12.features": [
+ "شبكة عُقَد متشابكة",
+ "جدول DHT لاكتشاف الأقران",
+ "توجيه بصلي مدمج",
+ "اقتصاد رمزي وحوافز للعُقَد",
+ "حوكمة عبر منظّمة لامركزية",
+ "قابلية التشغيل البيني مع شبكات أخرى",
+ "توافق عبر المنصّات",
+ "شبكة ذاتية التعافي"
+ ],
+ "roadmap.r13.title": "مساعد خصوصية بالذكاء الاصطناعي",
+ "roadmap.r13.sub": "ذكاء اصطناعي للخصوصية والأمان",
+ "roadmap.r13.date": "2028 وما بعدها",
+ "roadmap.r13.features": [
+ "تحليل تهديدات محلّي بالذكاء الاصطناعي",
+ "كشف تلقائي لهجمات الوسيط",
+ "تعمية تكيّفية",
+ "توصيات أمنية مخصّصة",
+ "تعلّم آلي بمعرفة صفرية",
+ "مساعد ذكاء اصطناعي خاصّ",
+ "أمان تنبّؤي",
+ "حماية ذاتية من الهجمات"
+ ],
+ "handshake.step1": "توليد زوج مفاتيح ECDH P-384",
+ "handshake.step2": "اشتقاق رمز التحقّق",
+ "handshake.step3": "تثبيت السرّية التامّة للأمام",
+ "handshake.securingTitle": "جارٍ تأمين قناتك",
+ "handshake.answerTitle": "جارٍ بناء ردّك",
+ "handshake.securingDesc": "نصوغ مفاتيح قويّة بما يكفي لمقاومة العبث.",
+ "handshake.shareTitle": "شارِك دعوتك",
+ "handshake.sendAnswerTitle": "أعد إرسال ردّك",
+ "handshake.shareDesc": "أظهر رمز QR أو أرسل الرمز إلى الطرف الآخر. يُستعمل مرّة واحدة وينتهي سريعاً.",
+ "handshake.sendAnswerDesc": "أعطِ هذا الردّ لمُنشئ القناة كي يُتمّ المصافحة.",
+ "handshake.establish": "إنشاء الاتصال",
+ "handshake.thenReceive": "ثمّ استقبل الردّ الذي يرسله الطرف الآخر",
+ "handshake.pasteAnswerPlaceholder": "ألصق رمز ردّ الطرف الآخر…",
+ "handshake.answerSentNote": "أرسل هذا الردّ إلى المُنشئ ثمّ انتظر - تُفتح الدردشة حالما يتّصل.",
+ "handshake.qrHint": "أبقِ هذا مفتوحاً حتى يلتقط الطرف الآخر الرمز.",
+ "handshake.qrHintFrames": "المصافحة موزّعة على {frames} إطارات - أبقِ هذا مفتوحاً حتى يلتقطها الطرف الآخر كلّها.",
+ "verify.title": "التحقّق الأمني",
+ "verify.desc": "قارِن رمز الأمان هذا مع الطرف الآخر عبر قناة منفصلة (مكالمة أو لقاء مباشر)، ثمّ اكتبه لفتح الدردشة.",
+ "verify.enterLabel": "أدخل الرمز المُتحقَّق منه",
+ "verify.placeholder": "اكتب الرمز هنا",
+ "verify.confirm": "تأكيد الرمز",
+ "verify.confirmed": "مؤكَّد",
+ "verify.mismatch": "غير متطابقين",
+ "verify.yours": "تأكيدك",
+ "verify.peer": "تأكيد الطرف الآخر",
+ "verify.pending": "قيد الانتظار",
+ "verify.waiting": "بانتظار الرمز…",
+ "verify.verified": "تمّ التحقّق من القناة",
+ "verify.bothConfirmed": "أكّد الطرفان. جارٍ فتح الدردشة الآمنة…",
+ "pwa.bannerTitle": "ثبّت SecureBit.chat",
+ "pwa.bannerDesc": "احصل على تجربة تطبيق أصلي بأمان أعلى",
+ "pwa.install": "تثبيت",
+ "pwa.dismiss": "تجاهل",
+ "pwa.close": "إغلاق",
+ "pwa.iosTitle": "التثبيت على iOS",
+ "pwa.iosStep1": "اضغط زرّ المشاركة",
+ "pwa.iosStep1Hint": "عادةً في أسفل سفاري",
+ "pwa.iosStep2": "ابحث عن «إضافة إلى الشاشة الرئيسية»",
+ "pwa.iosStep2Hint": "مرّر لأسفل في قائمة المشاركة",
+ "pwa.iosStep3": "اضغط «إضافة»",
+ "pwa.iosStep3Hint": "أكّد لتثبيت SecureBit.chat",
+ "pwa.genericTitle": "تثبيت SecureBit",
+ "pwa.genericDesc": "لكلّ متصفّح طريقته في التثبيت. اختر الخطوات التي تناسب متصفّحك.",
+ "pwa.gotIt": "فهمت",
+ "pwa.installedTitle": "تمّ تثبيت التطبيق!",
+ "pwa.installedIos": "تمّ تثبيت تطبيق iOS! افتحه من الشاشة الرئيسية.",
+ "pwa.installedGeneric": "صار SecureBit.chat على جهازك",
+ "pwa.anytimeTitle": "ثبّته في أيّ وقت",
+ "pwa.anytimeDesc": "لا يزال بإمكانك تثبيت SecureBit.chat من قائمة متصفّحك للحصول على أفضل تجربة.",
+ "pwa.ok": "حسناً",
+ "step.open": "الخطوة 1 · افتح قناة",
+ "step.exchange": "الخطوة 2 · التبادل",
+ "step.verification": "الخطوة 3 · التحقّق",
+ "action.create": "إنشاء",
+ "action.join": "انضمام",
+ "action.back": "رجوع",
+ "action.copy": "نسخ",
+ "action.copied": "تمّ النسخ",
+ "action.scan": "مسح",
+ "action.downloadDesktop": "نزّل تطبيق سطح المكتب",
+ "action.advancedSettings": "إعدادات متقدّمة",
+ "cred.offerTag": "عرض · أو انسخ النصّ",
+ "cred.answerTag": "ردّ · أو انسخ النصّ",
+ "cred.reveal": "اضغط للكشف - أبقِ هذا الرمز سرّياً",
+ "qr.title": "امسح رمز QR",
+ "qr.subtitle": "وجّه كاميرتك إلى رمزهم",
+ "qr.scanning": "جارٍ المسح…",
+ "qr.hint": "ثبّت يدك حتى تُلتقط كلّ الأجزاء. الوصول إلى الكاميرا محلّي - لا يُرفع شيء.",
+ "mesh.you": "أنت",
+ "mesh.peer": "طرف · جلسة 1",
+ "mesh.joined2": "انضمّت mara · +2",
+ "mesh.joined3": "انضمّ tobi · +3",
+ "ice.title": "إعدادات الشبكة",
+ "ice.subtitle": "تُضبط محلّياً - ولا تُشارَك أبداً مع الطرف الآخر",
+ "ice.intro": "يستخدم SecureBit خوادم STUN عامّة افتراضياً للتفاوض على الوصلة بين الطرفين. وجّهه إلى STUN/TURN الخاصّ بك إن كنت تستضيفه بنفسك.",
+ "ice.publicTitle": "خوادم عامّة (افتراضي)",
+ "ice.publicDesc": "بلا إعداد. مناسبة لمعظم المستخدمين.",
+ "ice.customTitle": "خوادم STUN/TURN الخاصّة بي",
+ "ice.customDesc": "حتى {max} خوادم.",
+ "ice.turnNote": "يرى مُرحِّل TURN عنوان IP لكلا الطرفين وتوقيت حركة البيانات - لكنه لا يرى محتوى الرسائل أبداً، فهي تبقى مشفّرة من طرف إلى طرف. يُفضَّل ",
+ "ice.turnNoteTls": " (TLS).",
+ "ice.test": "اختبر الخوادم",
+ "ice.testing": "جارٍ الاختبار…",
+ "ice.relayTitle": "وضع الترحيل فقط",
+ "ice.relayBadge": "أقصى خصوصية",
+ "ice.relayDesc": "يوجّه كلّ حركة البيانات عبر TURN فلا يُكشف عنوان IP للطرف الآخر أبداً. يتطلّب خادم TURN.",
+ "ice.relayWarning": "وضع الترحيل فقط مفعَّل لكن لا يوجد خادم TURN مضبوط. لن يتمكّن الاتصال من البدء.",
+ "ice.persist": "احفظ على هذا الجهاز",
+ "ice.persistDesc": "يُخزَّن مشفَّراً في هذا المتصفّح. اتركه مُطفأً لاستخدامه في هذه الجلسة فقط.",
+ "ice.forget": "انسَ المحفوظ",
+ "ice.cancel": "إلغاء",
+ "ice.apply": "تطبيق",
+ "ice.errUnavailable": "WebRTC غير متاح في هذا المتصفّح",
+ "ice.errInvalid": "إعداد خادم غير صالح",
+ "update.title": "يتوفّر تحديث",
+ "update.desc": "رُصد إصدار أحدث من SecureBit.",
+ "update.currentVersion": "الإصدار الحالي",
+ "update.newVersion": "الإصدار الجديد",
+ "update.now": "حدّث الآن",
+ "update.later": "لاحقاً",
+ "update.unknown": "غير متاح",
+ "update.stepSaving": "جارٍ حفظ البيانات...",
+ "update.stepSwCaches": "جارٍ مسح مخابئ عامل الخدمة...",
+ "update.stepSwUnregister": "جارٍ إلغاء تسجيل عوامل الخدمة...",
+ "update.stepBrowserCache": "جارٍ مسح ذاكرة المتصفّح المؤقّتة...",
+ "update.stepVersion": "جارٍ تحديث الإصدار...",
+ "update.stepReload": "جارٍ إعادة تحميل التطبيق...",
+ "update.error": "خطأ في التحديث. يُرجى تحديث الصفحة يدوياً (Ctrl+F5 أو Cmd+Shift+R)",
+ "update.confirmSkip": "يتوفّر إصدار جديد. يُنصح بالتحديث للأمان والاستقرار. أتريد المتابعة دون تحديث؟",
+ "offline.backOnline": "عاد الاتصال",
+ "offline.mode": "وضع دون اتصال",
+ "offline.dismiss": "تجاهل",
+ "offline.lostTitle": "انقطع الاتصال",
+ "offline.lostDesc": "SecureBit الآن في وضع دون اتصال. بعض المزايا محدودة، لكن بياناتك تبقى آمنة.",
+ "offline.point1": "جلستك ومفاتيحك محفوظة",
+ "offline.point2": "لا تُخزَّن أيّ بيانات على الخوادم",
+ "offline.point3": "تُزامَن الرسائل والملفّات عند عودة الاتصال",
+ "offline.continue": "تابع دون اتصال",
+ "offline.restoredTitle": "عند عودة اتصالك",
+ "offline.restoredDesc": "انقطاع الاتصال لا يكلّفك شيئاً. يضع SecureBit كلّ شيء في طابور محلّي ويستأنف الجلسة المشفّرة لحظة عودتك.",
+ "offline.r1Title": "تصل رسائلك",
+ "offline.r1Desc": "كلّ ما كتبته دون اتصال يُرسَل إلى مراسلك تلقائياً.",
+ "offline.r2Title": "تكتمل الملفّات",
+ "offline.r2Desc": "تُستأنف عمليات الرفع من حيث توقّفت - لا حاجة لإعادة الإرسال.",
+ "offline.r3Title": "تصلك رسائلهم وملفّاتهم",
+ "offline.r3Desc": "كلّ ما أرسله مراسلك أثناء الانقطاع يصلك بالترتيب.",
+ "offline.r4Title": "لا شيء يضيع",
+ "offline.r4Desc": "بعد عودة الاتصال لا توجد فجوة - تكمل المحادثة تماماً من حيث توقّفت.",
+ "offline.gotIt": "فهمت",
+ "qr.showTitle": "أظهر رمز QR",
+ "qr.showSubtitle": "ملء الشاشة · دع الطرف الآخر يمسحه",
+ "qr.showSubtitleFrames": "ملء الشاشة · دع الطرف الآخر يمسح الإطارات الـ {frames} كلّها",
+ "notify.enabledBody": "فُعِّلت الإشعارات! ستصلك تنبيهات بالرسائل الجديدة.",
+ "notify.workingBody": "الإشعارات تعمل! ستصلك تنبيهات بالرسائل الجديدة.",
+ "pw.label": "إدخال كلمة المرور",
+ "pw.placeholder": "أدخل كلمة المرور...",
+ "pw.decrypt": "فكّ التشفير",
+ "pw.cancel": "إلغاء",
+ "file.title": "نقل الملفّات",
+ "file.drop": "أسقط الملفّات هنا",
+ "file.dropHint": "تُشفَّر من طرف إلى طرف قبل النقل · حتى 100 ميغابايت",
+ "file.browse": "تصفّح الجهاز",
+ "file.incoming": "طلب ملفّ وارد",
+ "file.accept": "قبول",
+ "file.reject": "رفض",
+ "file.download": "تنزيل",
+ "file.notReady": "الاتصال غير جاهز",
+ "file.tooLarge": "الملفّ كبير جداً",
+ "file.typeNotAllowed": "نوع الملفّ غير مسموح",
+ "file.maxConcurrent": "الحدّ الأقصى للنقل المتزامن",
+ "file.gone": "لم يعد هذا الملفّ متاحاً للتنزيل.",
+ "call.incoming": "مكالمة واردة",
+ "call.incomingVideo": "مكالمة مرئية واردة",
+ "call.encrypted": "مكالمة مشفّرة",
+ "call.encryptedShort": "مشفّرة",
+ "call.peer": "طرف آمن",
+ "call.connecting": "جارٍ الاتصال…",
+ "call.ringing": "جارٍ الرنين…",
+ "call.accept": "قبول",
+ "call.decline": "رفض",
+ "call.end": "إنهاء المكالمة",
+ "call.camera": "الكاميرا",
+ "call.cameraOff": "الكاميرا مُطفأة",
+ "call.peerCameraOff": "كاميرا الطرف الآخر مُطفأة",
+ "call.flipCamera": "تبديل الكاميرا",
+ "call.addVideo": "أضف الفيديو",
+ "call.video": "فيديو",
+ "call.muted": "مكتوم",
+ "call.expand": "توسيع",
+ "call.minimize": "تصغير",
+ "call.quality": "جودة الاتصال",
+ "call.qualityExcellent": "ممتازة",
+ "call.videoPrefix": "مرئية · ",
+ "call.voicePrefix": "صوتية · ",
+ "group.new": "مجموعة جديدة",
+ "group.create": "أنشئ مجموعة",
+ "group.join": "انضمّ إلى مجموعة",
+ "group.name": "اسم المجموعة",
+ "group.nameTooLong": "اسم المجموعة طويل جداً.",
+ "group.capacity": "حتى {max} أشخاص، ندٌّ لندّ. سيقارن الجميع رمز أمان واحداً قبل فتح المجموعة.",
+ "group.invite": "دعوة",
+ "group.inviteMore": "ادعُ أعضاء آخرين",
+ "group.invitePeople": "ادعُ {count} أشخاص",
+ "group.invitation": "دعوة مجموعة",
+ "group.addMembers": "أضف أعضاء",
+ "group.roomFor": "يتّسع لـ {remaining} آخرين. سيقارن الجميع رمز مجموعة جديداً بمجرّد انضمامهم.",
+ "group.membersCount": "{count} أعضاء",
+ "group.member": "عضو",
+ "group.members": "الأعضاء",
+ "group.remove": "أزل {name}",
+ "group.message": "راسِل {name}",
+ "group.leave": "مغادرة",
+ "group.leaveThis": "غادر هذه المجموعة",
+ "group.cancel": "ألغِ المجموعة",
+ "group.close": "إغلاق",
+ "group.cancelBtn": "إلغاء",
+ "group.decline": "رفض",
+ "group.working": "جارٍ العمل…",
+ "group.sasTitle": "رمز أمان المجموعة",
+ "group.sasEveryone": "يرى الجميع هذا الرمز",
+ "group.sasCompare": "قارِن رمز المجموعة مع كلّ عضو لفتح هذه المجموعة.",
+ "group.sasReadAloud": "اقرأ هذه الأرقام بصوت عالٍ إلى ",
+ "group.sasWarning": "إن قرأ عضو واحد رمزاً مختلفاً فهناك من يجلس بينكم. ألغِ المجموعة - لا تؤكّد.",
+ "group.confirmFirst": "أكّد رمز المجموعة أوّلاً",
+ "group.codeSuffix": " · رمز {code}",
+ "group.exchangingNonces": "جارٍ تبادل القيم العشوائية…",
+ "group.waitingCommit": "بانتظار التزام كلّ عضو…",
+ "group.waitingJoin": "بانتظار انضمام بقيّة الأعضاء…",
+ "group.emptyChat": "لا شيء هنا بعد. الرسائل موقَّعة من مُرسِلها وتنتقل عبر وصلة مشفّرة خاصّة بكلّ عضو.",
+ "group.noVerified": "لا توجد دردشات مُتحقَّق منها بعد. افتح دردشة ثنائية وقارِن رمز أمانها أوّلاً - المجموعة تُبنى من اتصالات فحصتها من قبل.",
+ "group.noMoreToAdd": "لا توجد دردشات مُتحقَّق منها أخرى لإضافتها. افتح دردشة ثنائية وقارِن رمز أمانها أوّلاً.",
+ "group.directLink": "وصلة مباشرة ندٌّ لندّ",
+ "group.noDirectLink": "لا وصلة مباشرة بعد - يُرحِّل عضو آخر الرسائل ريثما تُبنى واحدة",
+ "group.relayNote": "بعض الأعضاء ليست لديهم وصلة مباشرة بك بعد. تمرّ رسائلهم عبر عضو آخر يرى أنّكم تتحدّثون لكنه لا يستطيع قراءة ما وراء التوقيع ولا تغيير ما قلته. تواصل المجموعة محاولة وصلهم مباشرةً.",
+ "group.relayOnlyOff": "وضع الترحيل فقط مُطفأ، فيتّصل بك كلّ عضو مباشرةً ويعرف عنوان IP الخاصّ بك - بمن فيهم أعضاء دعاهم شخص آخر. فعّله من إعدادات الشبكة إن كان ذلك يهمّك هنا.",
+ "group.memberOffline": "{name} غير متّصل ولن يتلقّى الرسائل. لا يزال عضواً - إزالته تُعيد توليد مفاتيح المجموعة.",
+ "group.joinNote": "سيعرف بقيّة الأعضاء بوجودك في هذه المجموعة. لا سجلّ رسائل لتلحق به - المجموعة تبدأ فارغة.",
+ "group.inviteSentNote": "تواصل المجموعة عملها حتى يقبلوا. لا سجلّ يلحقون به - لن يروا إلّا ما يُرسَل من الآن.",
+ "group.errNotCreated": "لم تُنشأ المجموعة",
+ "group.errNobodyAccepted": "لم يقبل أحد الدعوة في الوقت المحدّد.",
+ "group.errNothingSent": "لم يُرسَل شيء ولم يُتحقَّق من شيء. أغلق هذا وحاول مجدّداً بعد اتصال الجميع.",
+ "group.errNotFormed": "تعذّر تكوين هذه المجموعة.",
+ "group.errFull": "هذه المجموعة ممتلئة.",
+ "group.errInviteFailed": "تعذّر إرسال الدعوة - تلك الدردشة غير متّصلة.",
+ "group.errNoMemberList": "لم يرسل مالك المجموعة قائمة الأعضاء قطّ.",
+ "group.errUnsignedList": "لم يوقّع مالك المجموعة قائمة الأعضاء. لا تُعِد المحاولة - أخبرهم.",
+ "group.errNotOwner": "حاول شخص غير مالك المجموعة تغيير الأعضاء.",
+ "hdr.tagline": "مشفَّر من طرف إلى طرف",
+ "hdr.netSettings": "إعدادات شبكة متقدّمة",
+ "hdr.netSettingsTitle": "إعدادات شبكة متقدّمة (STUN/TURN)",
+ "hdr.disconnect": "قطع الاتصال",
+ "status.connected": "متّصل",
+ "status.connecting": "جارٍ الاتصال...",
+ "status.notConnected": "غير متّصل",
+ "status.reconnecting": "جارٍ إعادة الاتصال...",
+ "status.retrying": "جارٍ إعادة المحاولة...",
+ "status.verifying": "جارٍ التحقّق...",
+ "status.peerDisconnected": "انقطع الطرف الآخر",
+ "sec.replayProtection": "الحماية من إعادة التشغيل",
+ "sec.messageIntegrity": "سلامة الرسالة (HMAC)",
+ "sec.forwardSecrecy": "السرّية التامّة للأمام",
+ "sec.metadataProtection": "حماية البيانات الوصفية",
+ "sec.trafficObfuscation": "تمويه حركة البيانات",
+ "sec.realTests": "اختبارات تعمية حقيقية",
+ "sec.simulatedData": "بيانات محاكاة",
+ "sec.testPassed": "نجح الاختبار",
+ "sec.testFailed": "فشل الاختبار أو غير متاح",
+ "sec.verificationDone": "اكتمل التحقّق التعموي الحقيقي",
+ "sec.verificationInProgress": "التحقّق الأمني جارٍ...",
+ "sec.verificationWait": "التحقّق الأمني جارٍ...\nيُرجى انتظار اكتمال التحقّق التعموي الآني.",
+ "sec.verificationUnavailable": "التحقّق الأمني غير متاح",
+ "chat.placeholder": "اكتب رسالة مشفّرة…",
+ "chat.send": "أرسل الرسالة",
+ "chat.sendFiles": "أرسل ملفّات",
+ "chat.hideFiles": "أخفِ الملفّات",
+ "chat.recordVoice": "سجّل رسالة صوتية",
+ "chat.sendVoice": "أرسل الرسالة الصوتية",
+ "chat.discard": "تجاهل",
+ "chat.codeBlock": "أرسل ككتلة شيفرة (يوسّع حقل الإدخال)",
+ "chat.codeHint": "مقتطف شيفرة · التنسيق محفوظ · ⌘↵ للإرسال",
+ "chat.viewOnce": "عرض لمرّة",
+ "chat.viewOnceTitle": "عرض لمرّة - يختفي بعد أن يقرأه الطرف الآخر",
+ "chat.viewOncePrefix": "عرض لمرّة · ",
+ "chat.viewOnceTap": "عرض لمرّة · اضغط للكشف",
+ "chat.viewedOnce": "شُوهد مرّة",
+ "chat.disappearing": "رسالة تختفي - تُحذف عند الطرفين",
+ "chat.disappearAfter": "تختفي بعد",
+ "chat.visibleFor": "ظاهرة لمدّة",
+ "chat.timerPrefix": "مؤقّت · ",
+ "chat.expired": "انتهت صلاحية هذه الرسالة",
+ "chat.deleteForEveryone": "احذف عند الجميع",
+ "chat.sending": "جارٍ الإرسال",
+ "chat.delivered": "وصلت",
+ "chat.notSent": "لم تُرسل",
+ "chat.notSentReason": "لم تُرسل - القناة الآمنة ليست جاهزة. أعد الاتصال للمتابعة.",
+ "chat.encrypted": "مشفّرة",
+ "chat.decrypted": "فُكّ تشفيرها",
+ "chat.encryptedOnDevice": "مشفّرة على جهازك",
+ "chat.uploading": "جارٍ الرفع",
+ "chat.downloading": "جارٍ التنزيل",
+ "chat.transferring": "جارٍ النقل…",
+ "chat.copied": "تمّ النسخ!",
+ "chat.collapse": "طيّ",
+ "chat.closeMenu": "أغلق القائمة",
+ "chat.newChat": "دردشة جديدة",
+ "chat.newGroup": "مجموعة جديدة",
+ "chat.createGroup": "أنشئ مجموعة",
+ "chat.groupChats": "الدردشات الجماعية",
+ "chat.secureChat": "دردشة آمنة",
+ "chat.nameThis": "سمِّ هذه الدردشة",
+ "chat.rename": "أعد تسمية الدردشة (محلّياً فقط)",
+ "chat.renameHint": "انقر نقرتين لإعادة التسمية",
+ "chat.localLabel": "تسمية محلّية · مخزَّنة على هذا الجهاز فقط",
+ "chat.setStatus": "اضبط حالتك",
+ "chat.yourStatusPrefix": "حالتك - ",
+ "chat.meshHint": "حتى 8 أطراف · شبكة ندٍّ لندّ",
+ "chat.offline": "غير متّصل",
+ "chat.noNetwork": "لا شبكة · جارٍ إعادة الاتصال",
+ "chat.startVoiceCall": "ابدأ مكالمة صوتية مشفّرة",
+ "chat.startVideoCall": "ابدأ مكالمة مرئية مشفّرة",
+ "chat.verifyForCalls": "تحقّق من الجلسة لتفعيل المكالمات",
+ "chat.e2eeNote": "كلّ رسالة تُشفَّر من طرف إلى طرف على جهازك قبل أن تغادره.",
+ "chat.peersOnly": "تُرسَل من طرف إلى طرف إلى الأقران المتّصلين فقط - ولا تُخزَّن على خادم أبداً.",
+ "flow.invitationCreated": "أُنشئت دعوة آمنة",
+ "flow.invitationCreatedBang": "أُنشئت دعوة آمنة ومشفّرة!",
+ "flow.responseCreated": "أُنشئ ردّ آمن",
+ "flow.responseCreatedBang": "أُنشئ ردّ آمن!",
+ "flow.sendInvitation": "أرسل رمز الدعوة إلى محدّثك عبر قناة آمنة (مكالمة صوتية أو رسالة نصّية وما إلى ذلك).",
+ "flow.sendResponse": "أرسل رمز الردّ إلى البادئ عبر قناة آمنة أو دعه يمسح رمز QR أدناه.",
+ "flow.sendEncryptedCode": "أرسل الرمز المشفّر",
+ "flow.sendTheResponse": "أرسل الردّ",
+ "flow.pasteOrWrite": "ألصق الرمز أو اكتبه…",
+ "flow.processingInvitation": "جارٍ معالجة الدعوة الآمنة...",
+ "flow.processingResponse": "جارٍ معالجة الردّ الآمن...",
+ "flow.finalizing": "جارٍ إتمام الاتصال الآمن...",
+ "flow.channelEstablished": "أُنشئت القناة الآمنة",
+ "flow.channelReady": "القناة الآمنة جاهزة",
+ "flow.restoring": "جارٍ استعادة الاتصال…",
+ "flow.invitationCaptured": "التُقطت الدعوة.",
+ "err.needInvitation": "عليك إدراج رمز الدعوة من محدّثك.",
+ "err.needResponse": "عليك إدراج رمز الردّ من محدّثك.",
+ "err.responseFormat": "صيغة الردّ غير صالحة - يُرجى التحقّق من الرمز",
+ "err.responseNoKey": "رمز الردّ غير صالح - مفتاح التعمية مفقود أو تالف. يُرجى التحقّق من الرمز والمحاولة مجدّداً.",
+ "err.responseNoSignKey": "رمز الردّ غير صالح - مفتاح التحقّق من التوقيع مفقود. يُرجى التحقّق من الرمز والمحاولة مجدّداً.",
+ "err.responseOutdated": "بيانات الردّ قديمة - يُرجى استخدام دعوة جديدة",
+ "err.setupError": "خطأ في تهيئة الاتصال",
+ "err.notReady": "الاتصال غير جاهز",
+ "err.securityBreach": "رُصد خرق أمني - رُفض الاتصال",
+ "err.securityValidation": "فشل التحقّق الأمني - يُحتمل وجود هجوم",
+ "err.retiredQr": "يستخدم رمز QR هذا صيغة متقاعدة تعذّر معها نقل الدعوة. اطلب من الطرف الآخر توليد رمز جديد، أو استخدم النسخ واللصق.",
+ "err.compressedQrNote": "قد يُغفل رمز QR المضغوط بيانات SDP اختصاراً. استخدم النسخ واللصق إن فشل الاتصال.",
+ "err.inviteNotConnected": "تعذّر إرسال الدعوة. تلك الدردشة غير متّصلة الآن.",
+ "err.inviteNotConnectedRetry": "تعذّر إرسال الدعوة. تلك الدردشة غير متّصلة الآن - أعد فتحها وحاول مجدّداً.",
+ "err.nobodyAccepted": "لم يقبل أحد الدعوة. المجموعة على حالها.",
+ "err.noAudio": "لم يُلتقط أيّ صوت - تحقّق من إذن الميكروفون وحاول مجدّداً.",
+ "err.voiceNeedsConnection": "تحتاج الرسالة الصوتية إلى اتصال آمن نشط. أعد الاتصال وحاول مجدّداً.",
+ "err.voiceRestoring": "جارٍ استعادة الاتصال - حاول إرسال الرسالة الصوتية بعد لحظة.",
+ "err.fileTooLarge": "الملفّ كبير جداً",
+ "sas.safetyNumber": "رقم الأمان",
+ "sas.incorrect": "رمز غير صحيح. تحقّق منه مع الطرف الآخر وحاول مجدّداً.",
+ "sas.noMatch": "الرمزان غير متطابقين",
+ "sas.makeSure": "تأكّد من تطابق الرمزين تماماً.",
+ "sas.tooManyAttempts": "محاولات خاطئة كثيرة. أُعيدت الجلسة إلى نقطة البدء حفاظاً على الأمان.",
+ "sas.verificationFailed": "فشل التحقّق",
+ "sas.verified": "مُتحقَّق منه · سرّية تامّة للأمام",
+ "sas.runVerification": "شغّل التحقّق الأمني",
+ "sec.panelTitle": "تفاصيل الشبكة والتعمية",
+ "sec.security": "الأمان",
+ "sec.transport": "النقل",
+ "sec.keyExchange": "تبادل المفاتيح",
+ "sec.allEnabled": "كلّ مزايا الأمان مفعَّلة افتراضياً",
+ "sec.realTimeNote": "تحقّق آنيّ باستخدام دوال تعمية حقيقية - لا بيانات وهمية.",
+ "sec.simulatedWarning": "تحذير: قد لا يكون الاتصال قائماً بالكامل - قد تكون القيم محاكاة.",
+ "dl.title": "نزّل SecureBit",
+ "dl.free": "مجّاني · مفتوح المصدر",
+ "dl.soon": "تطبيقات الهواتف (iOS وأندرويد) وإضافات المتصفّحات (Chrome وFirefox وOpera) قادمة قريباً.",
+ "chatHdr.chat": "دردشة",
+ "chatHdr.chats": "الدردشات",
+ "chatHdr.newChat": "+ جديدة",
+ "chatHdr.secure": "آمنة",
+ "chatHdr.p2pSub": "ندٌّ لندّ · مشفَّرة من طرف إلى طرف",
+ "chatHdr.save": "حفظ",
+ "chatHdr.close": "إغلاق",
+ "chatHdr.expand": "توسيع",
+ "chatHdr.you": "أنت",
+ "chatHdr.qrCode": "رمز QR",
+ "chat.disconnected": "غير متّصل",
+ "call.mute": "كتم",
+ "call.endShort": "إنهاء",
+ "group.add": "إضافة",
+ "group.send": "إرسال",
+ "sec.pfsShort": "السرّية التامّة للأمام",
+ "sec.cipher": "الشيفرة",
+ "chat.close": "إغلاق",
+ "file.needConnection": "نقل الملفّات يحتاج إلى اتصال مفتوح",
+ "presence.available": "متاح",
+ "presence.away": "بعيد",
+ "presence.busy": "مشغول",
+ "presence.offline": "غير متّصل",
+ "presence.invisible": "خفيّ",
+ "presence.availableDesc": "متّصل ويمكن الوصول إليه",
+ "presence.awayDesc": "خامل · ابتعد قليلاً",
+ "presence.busyDesc": "الرجاء عدم الإزعاج",
+ "presence.invisibleDesc": "تظهر غير متّصل للأقران",
+ "presence.online": "متّصل",
+ "conn.p2p": "ندٌّ لندّ · متّصل",
+ "conn.verifying": "جارٍ التحقّق…",
+ "conn.connecting": "جارٍ الاتصال…",
+ "conn.reconnecting": "جارٍ إعادة الاتصال…",
+ "conn.peerDisconnected": "انقطع الطرف الآخر",
+ "conn.disconnected": "غير متّصل",
+ "chat.defaultLabel": "دردشة",
+ "groupPhase.forming": "جارٍ التكوين…",
+ "groupPhase.commitments": "جارٍ تبادل الالتزامات…",
+ "groupPhase.revealing": "جارٍ الكشف…",
+ "groupPhase.compare": "قارِن رمز المجموعة",
+ "groupPhase.ready": "المجموعة جاهزة",
+ "groupPhase.failed": "فشلت المجموعة",
+ "call.qualityGood": "جيّدة",
+ "call.qualityFair": "مقبولة",
+ "call.qualityWeak": "ضعيفة",
+ "msg.code": "شيفرة",
+ "msg.timer": "مؤقّت",
+ "msg.voice": "صوت",
+ "msg.play": "تشغيل",
+ "msg.sent": "أُرسلت",
+ "msg.read": "قُرئت",
+ "report.title": "تحقّق أمني آنيّ",
+ "report.active": "نشط",
+ "report.testsPassed": "اختبارات ناجحة",
+ "report.verifiedAt": "تمّ التحقّق في",
+ "report.source": "المصدر",
+ "secTest.verifyECDHKeyExchange": "تبادل مفاتيح ECDH",
+ "secTest.verifyECDSASignatures": "تواقيع ECDSA الرقمية",
+ "secTest.verifyEncryption": "تشفير AES-GCM",
+ "secTest.verifyMessageIntegrity": "سلامة الرسالة",
+ "secTest.verifyPerfectForwardSecrecy": "السرّية التامّة للأمام",
+ "secTest.verifyPFS": "السرّية التامّة للأمام",
+ "secTest.verifyReplayProtection": "الحماية من إعادة التشغيل",
+ "secTest.verifyDTLSFingerprint": "بصمة DTLS",
+ "secTest.verifySASVerification": "تحقّق SAS",
+ "secTest.verifyMetadataProtection": "حماية البيانات الوصفية",
+ "secTest.verifyTrafficObfuscation": "تمويه حركة البيانات",
+ "secTest.verifyPacketPadding": "حشو الحزم",
+ "secTest.verifyNestedEncryption": "التشفير المتداخل",
+ "secTest.verifyNonExtractableKeys": "مفاتيح غير قابلة للاستخراج",
+ "secTest.verifyRateLimiting": "تحديد المعدّل",
+ "secTest.verifyMutualAuth": "استيثاق متبادل",
+ "secTest.verifyAuthProof": "برهان الاستيثاق",
+ "secTest.verifyEnhancedValidation": "تحقّق مُعزَّز",
+ "secTest.verifyAdvancedFeatures": "مزايا متقدّمة",
+ "secTest.verifySignature": "فحص التوقيع",
+ "secDetail.Security system initializing...": "جارٍ تهيئة نظام الأمان...",
+ "secDetail.Nested encryption active": "التشفير المتداخل نشط",
+ "secDetail.Nested encryption failed": "فشل التشفير المتداخل",
+ "secDetail.Packet padding active": "حشو الحزم نشط",
+ "secDetail.Packet padding failed": "فشل حشو الحزم",
+ "secDetail.Advanced features active": "المزايا المتقدّمة نشطة",
+ "secDetail.Advanced features failed": "فشلت المزايا المتقدّمة",
+ "secDetail.No encryption key available": "لا يتوفّر مفتاح تشفير",
+ "secDetail.AES-GCM encryption/decryption working correctly": "تشفير وفكّ تشفير AES-GCM يعملان بشكل صحيح",
+ "secDetail.No ECDH key pair available": "لا يتوفّر زوج مفاتيح ECDH",
+ "secDetail.Key derivation failed": "فشل اشتقاق المفتاح",
+ "secDetail.No ECDSA key pair available": "لا يتوفّر زوج مفاتيح ECDSA",
+ "secDetail.ECDSA digital signatures working correctly": "تواقيع ECDSA الرقمية تعمل بشكل صحيح",
+ "secDetail.MAC key not available or invalid": "مفتاح MAC غير متاح أو غير صالح",
+ "secDetail.Rate limiter did not block a message over the limit": "لم يمنع محدِّد المعدّل رسالة تجاوزت الحدّ",
+ "secDetail.Rate limiter is not available": "محدِّد المعدّل غير متاح",
+ "secDetail.Replay protection is working correctly": "الحماية من إعادة التشغيل تعمل بشكل صحيح",
+ "secDetail.Replay protection not enabled": "الحماية من إعادة التشغيل غير مفعَّلة",
+ "secDetail.SAS code not available": "رمز SAS غير متاح",
+ "secDetail.SAS verification code is valid and available": "رمز تحقّق SAS صالح ومتاح",
+ "secDetail.Traffic obfuscation is working correctly": "تمويه حركة البيانات يعمل بشكل صحيح",
+ "secDetail.Traffic obfuscation not enabled": "تمويه حركة البيانات غير مفعَّل",
+ "secDetail.No non-extractable ephemeral ECDH key pair for this session": "لا يوجد زوج مفاتيح ECDH عابر غير قابل للاستخراج لهذه الجلسة",
+ "secDetail.Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation": "سرّية للأمام على مستوى الجلسة فقط: المفاتيح عابرة لكلّ جلسة، لكنّ السقّاطة المزدوجة غير نشطة في هذا الاتصال (الطرف الآخر على إصدار أقدم)، فتسريب مفتاح جلسة واحد يكشف المحادثة كلّها",
+ "msg.pause": "إيقاف مؤقّت",
+ "msg.failed": "فشلت",
+ "groupErr.timeout": "توقّف عضو عن الاستجابة قبل أن يجهز الرمز.",
+ "groupErr.keyMismatch": "لم يطابق مفتاح أحد الأعضاء الهويّة المدّعاة له.",
+ "groupErr.revealMismatch": "لم تطابق القيمة التي كشفها أحد الأعضاء ما التزم به.",
+ "groupErr.commitChanged": "غيّر أحد الأعضاء التزامه في منتصف الطريق.",
+ "groupErr.missingKey": "أُدرج عضو لم يصل مفتاحه قطّ.",
+ "groupErr.outsider": "وصل إطار من شخص خارج المجموعة.",
+ "groupErr.limit": "المجموعة محدودة بثمانية أعضاء.",
+ "groupErr.tooLarge": "كانت الرسالة أكبر من أن تُرسَل إلى المجموعة.",
+ "fileType.text": "نصّ عادي",
+ "fileType.images": "صور",
+ "fileType.archives": "أرشيفات",
+ "fileType.voice": "رسائل صوتية",
+ "fileType.unsupported": "غير مدعوم",
+ "qrScan.title": "امسح رمز QR",
+ "qrScan.auto": "الوضع التلقائي",
+ "qrScan.reset": "إعادة تعيين",
+ "qrScan.autoScroll": "التمرير التلقائي مفعَّل",
+ "qrScan.starting": "جارٍ تشغيل الكاميرا...",
+ "qrScan.point": "وجّه الكاميرا إلى رمز QR",
+ "qrScan.tapFocus": "اضغط الشاشة لضبط التركيز",
+ "qrScan.focusing": "جارٍ ضبط التركيز...",
+ "iceErr.notArray": "يجب أن تكون قائمة الخوادم مصفوفة",
+ "iceErr.invalidJson": "JSON غير صالح",
+ "iceErr.tooMany": "خوادم أكثر من اللازم (الحدّ الأقصى {max})",
+ "iceErr.invalidEntry": "{label}: مُدخَل غير صالح",
+ "iceErr.urlCount": "{label}: مطلوب بين 1 و{max} عنوان",
+ "iceErr.turnCreds": "{label}: عادةً تتطلّب خوادم TURN اسم مستخدم وبيانات اعتماد",
+ "iceUrl.notString": "يجب أن يكون العنوان نصّاً",
+ "iceUrl.empty": "العنوان فارغ",
+ "iceUrl.tooLong": "العنوان طويل جداً",
+ "iceUrl.badChars": "العنوان يحتوي على محارف غير صالحة",
+ "iceUrl.badScheme": "يجب أن يبدأ العنوان بـ stun: أو stuns: أو turn: أو turns:",
+ "iceUrl.badQuery": "استعلام العنوان غير صالح",
+ "iceUrl.noHost": "العنوان ينقصه المضيف",
+ "iceUrl.badHost": "مضيف العنوان أو منفذه غير صالح",
+ "iceUrl.badTransport": "يجب أن يكون استعلام العنوان transport=udp أو transport=tcp",
+ "msg.off": "مُطفأ",
+ "file.consentUnavailable": "موافقة المستخدم غير متاحة",
+ "secLevel.MAXIMUM": "الأقصى",
+ "secLevel.INITIALIZING": "قيد التهيئة",
+ "secLevel.ERROR": "خطأ",
+ "secLevel.UNKNOWN": "غير معروف",
+ "status.error": "خطأ",
+ "offline.disconnect": "قطع الاتصال",
+ "offline.learnMore": "اعرف المزيد",
+ "pwa.installApp": "ثبّت التطبيق",
+ "chat.onWeb": "أنت على الويب"
+ },
+ "he": {
+ "language.label": "שפה",
+ "community.title": "הצטרפו לעתיד של הפרטיות",
+ "community.description": "SecureBit גדל בזכות הקהילה שלו. הרעיונות והמשוב שלכם מעצבים את עתיד התקשורת המאובטחת - נבנה בגלוי, עם אימות ASN.1 מלא מקצה לקצה.",
+ "community.github": "מאגר GitHub",
+ "community.feedback": "משוב",
+ "unique.eyebrow": "מה מייחד אותנו",
+ "unique.heading": "למה SecureBit ייחודי",
+ "unique.s1.titleTop": "ליבת הצפנה",
+ "unique.s1.titleBottom": "רב־שכבתית",
+ "unique.s1.collapsed": "ליבת ההצפנה",
+ "unique.s1.desc": "חילופי מפתחות ECDH P-384, מטענים ב-AES-256-GCM, חתימות ECDSA ואימות ASN.1 מלא - מורכבים לצינור מוקשח אחד.",
+ "unique.s1.tags": [
+ "ECDH P-384",
+ "AES-256-GCM",
+ "ECDSA",
+ "ASN.1"
+ ],
+ "unique.s2.titleTop": "עמית־לעמית",
+ "unique.s2.titleBottom": "טהור ב-WebRTC",
+ "unique.s2.collapsed": "WebRTC עמית־לעמית טהור",
+ "unique.s2.desc": "ההודעות עוברות ישירות בין המכשירים דרך WebRTC. אף ממסר לא מחזיק את הנתונים שלכם - השרת רק עוזר לשני עמיתים למצוא זה את זה.",
+ "unique.s2.tags": [
+ "DTLS 1.3",
+ "בלי ממסר"
+ ],
+ "unique.s3.titleTop": "סודיות קדימה",
+ "unique.s3.titleBottom": "מושלמת",
+ "unique.s3.collapsed": "סודיות קדימה",
+ "unique.s3.desc": "מפתחות הפעלה מתחלפים ברציפות ונמחקים לאחר השימוש, כך שמפתח אחד שנחשף לעולם לא יפתח שיחות מהעבר.",
+ "unique.s3.tags": [
+ "מפתחות זמניים",
+ "החלפה אוטומטית"
+ ],
+ "unique.s4.titleTop": "ערפול",
+ "unique.s4.titleBottom": "תעבורה",
+ "unique.s4.collapsed": "ערפול תעבורה",
+ "unique.s4.desc": "גדלי המנות והתזמונים מרופדים ומעורבלים, ומסתירים דפוסי מטא־נתונים מכל מי שמאזין לקו.",
+ "unique.s4.tags": [
+ "ריפוד מנות",
+ "רעש בתזמון"
+ ],
+ "unique.s5.titleTop": "אפס",
+ "unique.s5.titleBottom": "איסוף נתונים",
+ "unique.s5.collapsed": "אפס איסוף נתונים",
+ "unique.s5.desc": "בלי חשבונות, בלי יומנים, בלי אחסון הודעות. אין שום דבר בשרת שאפשר להדליף, לזמן בצו או למכור.",
+ "unique.s5.tags": [
+ "בלי חשבונות",
+ "בלי יומנים"
+ ],
+ "partners.eyebrow": "שותפים ואקוסיסטם",
+ "partners.heading": "השותפים שלנו סומכים עלינו",
+ "partners.aegis.desc": "שותף הון שמאבטח תקשורת פיננסית חסויה בכל תיק ההשקעות שלו.",
+ "partners.aegis.role": "תומך אסטרטגי",
+ "partners.furilabs.desc": "טלפוני לינוקס שמעמידים פרטיות בראש ומשלבים את SecureBit כערוץ מאובטח כברירת מחדל.",
+ "partners.furilabs.role": "שותף טכנולוגי",
+ "partners.inviteTitle": "היו שותפים",
+ "partners.inviteDesc": "בונים חומרה או תשתית לפרטיות? בואו נשלב את SecureBit.",
+ "partners.inviteCta": "פתחו שיחה",
+ "roadmap.eyebrow": "מפת דרכים לפיתוח",
+ "roadmap.heading": "ההתפתחות של SecureBit",
+ "roadmap.subheading": "מהאב־טיפוס הראשון ועד רשת מבוזרת עמידה לקוונטים - עם אימות ASN.1 מלא בכל שכבה.",
+ "roadmap.progress": "{shipped} מתוך {total} אבני דרך הושלמו",
+ "roadmap.upcoming": "{upcoming} בדרך",
+ "roadmap.keyFeatures": "יכולות מרכזיות",
+ "roadmap.status.released": "שוחרר",
+ "roadmap.status.current": "נוכחי",
+ "roadmap.status.dev": "בפיתוח",
+ "roadmap.status.planned": "מתוכנן",
+ "roadmap.status.research": "מחקר",
+ "hero.headlineTop": "קו ישיר",
+ "hero.headlineBottom": "שרק שניכם יכולים לקרוא.",
+ "hero.subheading": "המפתחות נוצרים במכשיר שלכם ומוחלפים עמית־לעמית. בלי חשבונות, בלי שרתים ששומרים את ההודעות.",
+ "hero.diagramAlt": "קו מוצפן ישיר לעמית אחד, כששני עמיתים נוספים מצטרפים לרשת",
+ "intro.createTitle": "פתחו ערוץ חדש",
+ "intro.createDesc": "המכשיר שלכם יוצר את המפתחות והזמנה חד־פעמית. שום דבר לא נוגע בשרת.",
+ "intro.createCta": "יצירת מפתחות והזמנה",
+ "intro.joinTitle": "הצטרפו לערוץ",
+ "intro.scanTitle": "סרקו QR עם המצלמה",
+ "intro.scanSubtitle": "הכי מהיר - כוונו למסך של העמית",
+ "intro.orPasteCode": "או הדביקו קוד",
+ "intro.pastePlaceholder": "הדביקו כאן את קוד ההזמנה…",
+ "intro.connect": "התחברו",
+ "intro.connecting": "מעבד…",
+ "roadmap.r1.title": "תחילת הפיתוח",
+ "roadmap.r1.sub": "רעיון, אב־טיפוס והקמת תשתית",
+ "roadmap.r1.date": "תחילת 2025",
+ "roadmap.r1.features": [
+ "גיבוש הרעיון והדרישות",
+ "בחירת מחסנית: WebRTC, P2P, קריפטוגרפיה",
+ "אבות־טיפוס ראשונים למסרים",
+ "יצירת מאגר ואינטגרציה רציפה",
+ "ארכיטקטורת הצפנה בסיסית",
+ "עיצוב חוויה וממשק"
+ ],
+ "roadmap.r2.title": "גרסת אלפא",
+ "roadmap.r2.sub": "אלפא ציבורית ראשונה: צ'אט בסיסי וחילופי מפתחות",
+ "roadmap.r2.date": "אביב 2025",
+ "roadmap.r2.features": [
+ "מסרים עמית־לעמית בסיסיים דרך WebRTC",
+ "הצפנה פשוטה מקצה לקצה (סכמת הדגמה)",
+ "איתות יציב והתחברות מחדש",
+ "ממשק מינימלי לבדיקות",
+ "איסוף משוב מבודקים מוקדמים"
+ ],
+ "roadmap.r3.title": "חיזוק אבטחה",
+ "roadmap.r3.sub": "חיזוק האבטחה ושחרור ענף יציב",
+ "roadmap.r3.date": "קיץ 2025",
+ "roadmap.r3.features": [
+ "מימוש ECDH/ECDSA בסביבת ייצור",
+ "סודיות קדימה מושלמת והחלפת מפתחות",
+ "שיפור בדיקות האימות",
+ "הצפנת קבצים והעברת מטענים גדולים",
+ "ביקורת על תהליכי הקריפטו הבסיסיים"
+ ],
+ "roadmap.r4.title": "התרחבות ויציבות",
+ "roadmap.r4.sub": "הרחבת הרשת ושיפור היציבות",
+ "roadmap.r4.date": "סתיו 2025",
+ "roadmap.r4.features": [
+ "ייעול חיבורי P2P וחציית NAT",
+ "מנגנוני התחברות מחדש ותורי הודעות",
+ "צריכת סוללה מופחתת בנייד",
+ "תמיכה בסנכרון בין מכשירים",
+ "כלי ניטור ורישום למפתחים"
+ ],
+ "roadmap.r5.title": "גרסת פרטיות תחילה",
+ "roadmap.r5.sub": "התמקדות בפרטיות: מזעור מטא־נתונים",
+ "roadmap.r5.date": "חורף 2025",
+ "roadmap.r5.features": [
+ "הגנת מטא־נתונים והקטנת טביעת אצבע",
+ "ניסויים בניתוב בצל וב-DHT",
+ "אפשרויות לחיבורים אנונימיים",
+ "היערכות לביקורת קוד פתוחה",
+ "שיפור תהליכי אימות המשתמשים"
+ ],
+ "roadmap.r6.title": "מהדורת אבטחה מוגברת",
+ "roadmap.r6.sub": "קריפטוגרפיה ברמה צבאית ב-18 שכבות עם אימות ASN.1 מלא",
+ "roadmap.r6.date": "סוף 2025",
+ "roadmap.r6.features": [
+ "אבטחה תלת־שכבתית: ECDH, DTLS ו-SAS",
+ "הצפנת ECDH P-384 עם AES-GCM 256 סיביות",
+ "אימות טביעת אצבע DTLS",
+ "אימות SAS (מחרוזת אימות קצרה)",
+ "סודיות קדימה מושלמת עם החלפת מפתחות",
+ "מניעה מוגברת של התקפות אדם באמצע",
+ "אימות ASN.1 DER מלא",
+ "אימות OID ונקודות עקומה",
+ "אימות מבנה SPKI",
+ "ארכיטקטורת WebRTC עמית־לעמית",
+ "הגנת מטא־נתונים",
+ "קוד פתוח ב-100%"
+ ],
+ "roadmap.r7.title": "מהדורת שולחן עבודה",
+ "roadmap.r7.sub": "אפליקציות מקוריות ל-Windows, macOS ולינוקס",
+ "roadmap.r7.date": "תחילת 2026",
+ "roadmap.r7.features": [
+ "אפליקציית Windows (Tauri v2)",
+ "אפליקציית macOS (Tauri v2)",
+ "תמיכת AppImage בלינוקס (Tauri v2)",
+ "התראות בזמן אמת",
+ "התחברות מחדש אוטומטית",
+ "סנכרון בין מכשירים",
+ "שיפור חוויה וממשק",
+ "תמיכה בקבצים עד 100MB"
+ ],
+ "roadmap.r8.title": "קול ושיחות מאובטחות",
+ "roadmap.r8.sub": "הודעות קוליות, שיחות אודיו ווידאו מוצפנות",
+ "roadmap.r8.date": "תחילת 2026",
+ "roadmap.r8.features": [
+ "הודעות קוליות מוצפנות מקצה לקצה",
+ "שיחות אודיו מוצפנות 1:1 (WebRTC)",
+ "שיחות וידאו מוצפנות 1:1 (WebRTC)",
+ "סודיות קדימה מושלמת למדיה חיה",
+ "זרמי מדיה מוגני SRTP/DTLS",
+ "אימות SAS תוך כדי שיחה",
+ "התראות שיחה והתחברות מחדש אוטומטית",
+ "מדיה עמית־לעמית בהשהיה נמוכה"
+ ],
+ "roadmap.r9.title": "תקשורת קבוצתית",
+ "roadmap.r9.sub": "צ'אטים קבוצתיים תוך שמירה על הפרטיות",
+ "roadmap.r9.date": "עכשיו",
+ "roadmap.r9.features": [
+ "צ'אטים קבוצתיים עמית־לעמית עד 8 משתתפים",
+ "מסירה ברשת עם גיבוי ממסר חתום",
+ "קוד בטיחות קבוצתי אחד שכולם משווים",
+ "טקס התחייבות־ואז־חשיפה נגד ניחוש הקוד",
+ "מפתחות זהות לכל קבוצה, זמניים מעצם התכנון",
+ "חברוּת חתומה עם סדר לפי תקופות",
+ "הודעות חתומות, כך שתמליל מפוצל ניתן להוכחה",
+ "בלי שרת, בלי מפתח קבוצתי משותף, בלי היסטוריה"
+ ],
+ "roadmap.r10.title": "מהדורת נייד",
+ "roadmap.r10.sub": "אפליקציות מקוריות ל-iOS ואנדרואיד",
+ "roadmap.r10.date": "רבעון 2 2027",
+ "roadmap.r10.features": [
+ "אפליקציית iOS מקורית (Swift/SwiftUI)",
+ "אפליקציית אנדרואיד מקורית (Kotlin/Jetpack Compose)",
+ "תמיכת PWA בדפדפני נייד",
+ "התראות דחיפה בזמן אמת",
+ "אופטימיזציית סוללה",
+ "ממשק מותאם לנייד",
+ "תור הודעות במצב לא מקוון",
+ "אימות ביומטרי"
+ ],
+ "roadmap.r11.title": "מהדורה עמידה לקוונטים",
+ "roadmap.r11.sub": "הגנה מפני מחשבים קוונטיים",
+ "roadmap.r11.date": "רבעון 4 2027",
+ "roadmap.r11.features": [
+ "קריפטוגרפיה פוסט־קוונטית CRYSTALS-Kyber",
+ "חתימות דיגיטליות SPHINCS+",
+ "סכמה היברידית: קלאסי + פוסט־קוונטי",
+ "חילופי מפתחות בטוחים קוונטית",
+ "אלגוריתמי גיבוב מעודכנים",
+ "העברת הפעלות קיימות",
+ "תאימות ל-v5.x",
+ "פרוטוקולים עמידים לקוונטים"
+ ],
+ "roadmap.r12.title": "רשת מבוזרת",
+ "roadmap.r12.sub": "רשת מבוזרת לחלוטין",
+ "roadmap.r12.date": "2028",
+ "roadmap.r12.features": [
+ "רשת צמתים מקושרת",
+ "DHT לגילוי עמיתים",
+ "ניתוב בצל מובנה",
+ "טוקנומיקה ותמריצים לצמתים",
+ "ממשל באמצעות DAO",
+ "יכולת פעולה הדדית עם רשתות אחרות",
+ "תאימות חוצת פלטפורמות",
+ "רשת שמרפאת את עצמה"
+ ],
+ "roadmap.r13.title": "עוזר פרטיות מבוסס בינה מלאכותית",
+ "roadmap.r13.sub": "בינה מלאכותית לפרטיות ואבטחה",
+ "roadmap.r13.date": "2028 ואילך",
+ "roadmap.r13.features": [
+ "ניתוח איומים מקומי בבינה מלאכותית",
+ "זיהוי אוטומטי של אדם באמצע",
+ "קריפטוגרפיה אדפטיבית",
+ "המלצות אבטחה מותאמות אישית",
+ "למידת מכונה באפס ידע",
+ "עוזר בינה מלאכותית פרטי",
+ "אבטחה חזאית",
+ "הגנה אוטונומית מפני התקפות"
+ ],
+ "handshake.step1": "יוצר זוג מפתחות ECDH P-384",
+ "handshake.step2": "גוזר קוד אימות",
+ "handshake.step3": "מקבע סודיות קדימה מושלמת",
+ "handshake.securingTitle": "מאבטח את הערוץ שלכם",
+ "handshake.answerTitle": "בונה את התשובה שלכם",
+ "handshake.securingDesc": "מחשל מפתחות חזקים דיים כדי לעמוד בחבלה.",
+ "handshake.shareTitle": "שתפו את ההזמנה",
+ "handshake.sendAnswerTitle": "שלחו בחזרה את התשובה",
+ "handshake.shareDesc": "הציגו את ה-QR או שלחו את הקוד לעמית. הוא חד־פעמי ופג בקרוב.",
+ "handshake.sendAnswerDesc": "מסרו את התשובה הזו ליוצר הערוץ כדי שיוכל להשלים את לחיצת היד.",
+ "handshake.establish": "יצירת חיבור",
+ "handshake.thenReceive": "ואז קבלו את התשובה שהעמית שולח בחזרה",
+ "handshake.pasteAnswerPlaceholder": "הדביקו את קוד התשובה של העמית…",
+ "handshake.answerSentNote": "שלחו את התשובה ליוצר ואז המתינו - הצ'אט נפתח ברגע שהם מתחברים.",
+ "handshake.qrHint": "השאירו פתוח עד שהעמית יקלוט את הקוד.",
+ "handshake.qrHintFrames": "לחיצת היד מפוצלת ל-{frames} פריימים - השאירו פתוח עד שהעמית יקלוט את כולם.",
+ "verify.title": "אימות אבטחה",
+ "verify.desc": "השוו את קוד הבטיחות הזה עם העמית בערוץ נפרד (שיחה קולית / פנים אל פנים), ואז הקלידו אותו כדי לפתוח את הצ'אט.",
+ "verify.enterLabel": "הזינו את הקוד המאומת",
+ "verify.placeholder": "הקלידו כאן את הקוד",
+ "verify.confirm": "אשרו את הקוד",
+ "verify.confirmed": "אושר",
+ "verify.mismatch": "לא תואמים",
+ "verify.yours": "האישור שלכם",
+ "verify.peer": "אישור העמית",
+ "verify.pending": "ממתין",
+ "verify.waiting": "ממתין לקוד…",
+ "verify.verified": "הערוץ אומת",
+ "verify.bothConfirmed": "שני הצדדים אישרו. פותח את הצ'אט המאובטח…",
+ "pwa.bannerTitle": "התקינו את SecureBit.chat",
+ "pwa.bannerDesc": "קבלו חוויית אפליקציה מקורית עם אבטחה משופרת",
+ "pwa.install": "התקנה",
+ "pwa.dismiss": "ביטול",
+ "pwa.close": "סגירה",
+ "pwa.iosTitle": "התקנה ב-iOS",
+ "pwa.iosStep1": "הקישו על כפתור השיתוף",
+ "pwa.iosStep1Hint": "בדרך כלל בתחתית ספארי",
+ "pwa.iosStep2": "מצאו \"הוסף למסך הבית\"",
+ "pwa.iosStep2Hint": "גללו מטה בתפריט השיתוף",
+ "pwa.iosStep3": "הקישו על \"הוסף\"",
+ "pwa.iosStep3Hint": "אשרו כדי להתקין את SecureBit.chat",
+ "pwa.genericTitle": "התקנת SecureBit",
+ "pwa.genericDesc": "לכל דפדפן דרך משלו להתקין. בחרו את השלבים שמתאימים לשלכם.",
+ "pwa.gotIt": "הבנתי",
+ "pwa.installedTitle": "האפליקציה הותקנה!",
+ "pwa.installedIos": "אפליקציית iOS הותקנה! פתחו ממסך הבית.",
+ "pwa.installedGeneric": "SecureBit.chat נמצא עכשיו במכשיר שלכם",
+ "pwa.anytimeTitle": "התקינו מתי שתרצו",
+ "pwa.anytimeDesc": "עדיין אפשר להתקין את SecureBit.chat מתפריט הדפדפן לחוויה הטובה ביותר.",
+ "pwa.ok": "אישור",
+ "step.open": "שלב 1 · פתיחת ערוץ",
+ "step.exchange": "שלב 2 · חילופים",
+ "step.verification": "שלב 3 · אימות",
+ "action.create": "יצירה",
+ "action.join": "הצטרפות",
+ "action.back": "חזרה",
+ "action.copy": "העתקה",
+ "action.copied": "הועתק",
+ "action.scan": "סריקה",
+ "action.downloadDesktop": "הורידו את אפליקציית שולחן העבודה",
+ "action.advancedSettings": "הגדרות מתקדמות",
+ "cred.offerTag": "הצעה · או העתיקו טקסט",
+ "cred.answerTag": "תשובה · או העתיקו טקסט",
+ "cred.reveal": "לחצו לחשיפה - שמרו את הקוד הזה פרטי",
+ "qr.title": "סריקת קוד QR",
+ "qr.subtitle": "כוונו את המצלמה ל-QR שלהם",
+ "qr.scanning": "סורק…",
+ "qr.hint": "החזיקו יציב עד שכל החלקים ייקלטו. הגישה למצלמה מקומית - שום דבר לא מועלה.",
+ "mesh.you": "אתם",
+ "mesh.peer": "עמית · הפעלה 1",
+ "mesh.joined2": "mara הצטרפה · +2",
+ "mesh.joined3": "tobi הצטרף · +3",
+ "ice.title": "הגדרות רשת",
+ "ice.subtitle": "מוגדרות מקומית - לעולם לא משותפות עם העמית",
+ "ice.intro": "SecureBit משתמש כברירת מחדל בשרתי STUN ציבוריים כדי לתאם את הקישור בין העמיתים. הפנו אותו ל-STUN/TURN שלכם אם אתם מארחים בעצמכם.",
+ "ice.publicTitle": "שרתים ציבוריים (ברירת מחדל)",
+ "ice.publicDesc": "בלי הגדרות. מתאים לרוב המשתמשים.",
+ "ice.customTitle": "שרתי STUN/TURN משלי",
+ "ice.customDesc": "עד {max} שרתים.",
+ "ice.turnNote": "ממסר TURN רואה את כתובות ה-IP של שני העמיתים ואת תזמון התעבורה - אך לעולם לא את תוכן ההודעות, שנשאר מוצפן מקצה לקצה. עדיף ",
+ "ice.turnNoteTls": " (TLS).",
+ "ice.test": "בדקו את השרתים",
+ "ice.testing": "בודק…",
+ "ice.relayTitle": "מצב ריליי בלבד",
+ "ice.relayBadge": "פרטיות מרבית",
+ "ice.relayDesc": "מנתב את כל התעבורה דרך TURN כך שכתובת ה-IP שלכם לעולם לא נחשפת לעמית. דורש שרת TURN.",
+ "ice.relayWarning": "מצב ריליי בלבד מופעל אך לא הוגדר שרת TURN. החיבור לא יוכל להתחיל.",
+ "ice.persist": "שמרו במכשיר הזה",
+ "ice.persistDesc": "נשמר מוצפן בדפדפן הזה. השאירו כבוי כדי להשתמש רק בהפעלה הנוכחית.",
+ "ice.forget": "שכחו את השמור",
+ "ice.cancel": "ביטול",
+ "ice.apply": "החלה",
+ "ice.errUnavailable": "WebRTC אינו זמין בדפדפן הזה",
+ "ice.errInvalid": "תצורת שרת לא תקינה",
+ "update.title": "יש עדכון",
+ "update.desc": "זוהתה גרסה חדשה יותר של SecureBit.",
+ "update.currentVersion": "גרסה נוכחית",
+ "update.newVersion": "גרסה חדשה",
+ "update.now": "עדכנו עכשיו",
+ "update.later": "מאוחר יותר",
+ "update.unknown": "לא זמין",
+ "update.stepSaving": "שומר נתונים...",
+ "update.stepSwCaches": "מנקה מטמוני Service Worker...",
+ "update.stepSwUnregister": "מבטל רישום Service Workers...",
+ "update.stepBrowserCache": "מנקה את מטמון הדפדפן...",
+ "update.stepVersion": "מעדכן גרסה...",
+ "update.stepReload": "טוען מחדש את היישום...",
+ "update.error": "שגיאת עדכון. רעננו את הדף ידנית (Ctrl+F5 או Cmd+Shift+R)",
+ "update.confirmSkip": "יש גרסה חדשה. מומלץ לעדכן למען האבטחה והיציבות. להמשיך בלי לעדכן?",
+ "offline.backOnline": "חזרנו לרשת",
+ "offline.mode": "מצב לא מקוון",
+ "offline.dismiss": "ביטול",
+ "offline.lostTitle": "החיבור אבד",
+ "offline.lostDesc": "SecureBit במצב לא מקוון. חלק מהיכולות מוגבלות, אבל הנתונים שלכם נשארים בטוחים.",
+ "offline.point1": "ההפעלה והמפתחות שלכם נשמרים",
+ "offline.point2": "שום נתון לא נשמר בשרתים",
+ "offline.point3": "הודעות וקבצים מסתנכרנים כשתתחברו מחדש",
+ "offline.continue": "המשיכו במצב לא מקוון",
+ "offline.restoredTitle": "כשתתחברו מחדש",
+ "offline.restoredDesc": "ניתוק לא עולה לכם בכלום. SecureBit מציב הכל בתור מקומי ומחדש את ההפעלה המוצפנת ברגע שתחזרו לרשת.",
+ "offline.r1Title": "ההודעות שלכם נמסרות",
+ "offline.r1Desc": "כל מה שכתבתם במצב לא מקוון נשלח לאיש הקשר שלכם אוטומטית.",
+ "offline.r2Title": "הקבצים מסיימים לעבור",
+ "offline.r2Desc": "ההעלאות ממשיכות מהנקודה שבה נעצרו - אין צורך לשלוח שוב.",
+ "offline.r3Title": "ההודעות והקבצים שלהם מגיעים",
+ "offline.r3Desc": "כל מה שאיש הקשר שלח במהלך הניתוק נמסר לכם לפי הסדר.",
+ "offline.r4Title": "שום דבר לא הולך לאיבוד",
+ "offline.r4Desc": "אחרי ההתחברות מחדש אין פער - השיחה ממשיכה בדיוק מהמקום שבו נעצרה.",
+ "offline.gotIt": "הבנתי",
+ "qr.showTitle": "הצגת קוד QR",
+ "qr.showSubtitle": "מסך מלא · תנו לעמית לסרוק",
+ "qr.showSubtitleFrames": "מסך מלא · תנו לעמית לסרוק את כל {frames} הפריימים",
+ "notify.enabledBody": "ההתראות הופעלו! תקבלו התראות על הודעות חדשות.",
+ "notify.workingBody": "ההתראות עובדות! תקבלו התראות על הודעות חדשות.",
+ "pw.label": "הזנת סיסמה",
+ "pw.placeholder": "הזינו סיסמה...",
+ "pw.decrypt": "פענוח",
+ "pw.cancel": "ביטול",
+ "file.title": "העברות קבצים",
+ "file.drop": "גררו ושחררו קבצים כאן",
+ "file.dropHint": "מוצפנים מקצה לקצה לפני ההעברה · עד 100 MB",
+ "file.browse": "עיון במכשיר",
+ "file.incoming": "בקשת קובץ נכנסת",
+ "file.accept": "אישור",
+ "file.reject": "דחייה",
+ "file.download": "הורדה",
+ "file.notReady": "החיבור אינו מוכן",
+ "file.tooLarge": "הקובץ גדול מדי",
+ "file.typeNotAllowed": "סוג הקובץ אינו מורשה",
+ "file.maxConcurrent": "מקסימום העברות במקביל",
+ "file.gone": "הקובץ הזה כבר אינו זמין להורדה.",
+ "call.incoming": "שיחה נכנסת",
+ "call.incomingVideo": "שיחת וידאו נכנסת",
+ "call.encrypted": "שיחה מוצפנת",
+ "call.encryptedShort": "מוצפנת",
+ "call.peer": "עמית מאובטח",
+ "call.connecting": "מתחבר…",
+ "call.ringing": "מצלצל…",
+ "call.accept": "מענה",
+ "call.decline": "דחייה",
+ "call.end": "סיום שיחה",
+ "call.camera": "מצלמה",
+ "call.cameraOff": "המצלמה כבויה",
+ "call.peerCameraOff": "המצלמה של העמית כבויה",
+ "call.flipCamera": "החלפת מצלמה",
+ "call.addVideo": "הוספת וידאו",
+ "call.video": "וידאו",
+ "call.muted": "מושתק",
+ "call.expand": "הגדלה",
+ "call.minimize": "מזעור",
+ "call.quality": "איכות החיבור",
+ "call.qualityExcellent": "מצוינת",
+ "call.videoPrefix": "וידאו · ",
+ "call.voicePrefix": "קול · ",
+ "group.new": "קבוצה חדשה",
+ "group.create": "יצירת קבוצה",
+ "group.join": "הצטרפות לקבוצה",
+ "group.name": "שם הקבוצה",
+ "group.nameTooLong": "שם הקבוצה ארוך מדי.",
+ "group.capacity": "עד {max} אנשים, עמית־לעמית. כולם ישוו קוד בטיחות אחד לפני שהקבוצה נפתחת.",
+ "group.invite": "הזמנה",
+ "group.inviteMore": "הזמינו חברים נוספים",
+ "group.invitePeople": "הזמינו {count} אנשים",
+ "group.invitation": "הזמנה לקבוצה",
+ "group.addMembers": "הוספת חברים",
+ "group.roomFor": "יש מקום ל-{remaining} נוספים. כולם ישוו קוד קבוצתי חדש ברגע שיצטרפו.",
+ "group.membersCount": "{count} חברים",
+ "group.member": "חבר",
+ "group.members": "חברים",
+ "group.remove": "הסירו את {name}",
+ "group.message": "שלחו הודעה ל{name}",
+ "group.leave": "יציאה",
+ "group.leaveThis": "צאו מהקבוצה הזו",
+ "group.cancel": "ביטול הקבוצה",
+ "group.close": "סגירה",
+ "group.cancelBtn": "ביטול",
+ "group.decline": "דחייה",
+ "group.working": "עובד…",
+ "group.sasTitle": "קוד בטיחות קבוצתי",
+ "group.sasEveryone": "כולם רואים את הקוד הזה",
+ "group.sasCompare": "השוו את הקוד הקבוצתי עם כל חבר כדי לפתוח את הקבוצה.",
+ "group.sasReadAloud": "הקריאו את הספרות האלה בקול ל",
+ "group.sasWarning": "אם אפילו חבר אחד מקריא קוד שונה, מישהו יושב ביניכם. בטלו את הקבוצה - אל תאשרו.",
+ "group.confirmFirst": "אשרו קודם את הקוד הקבוצתי",
+ "group.codeSuffix": " · קוד {code}",
+ "group.exchangingNonces": "מחליף ערכים חד־פעמיים…",
+ "group.waitingCommit": "ממתין לכל חבר שיתחייב…",
+ "group.waitingJoin": "ממתין לשאר החברים שיצטרפו…",
+ "group.emptyChat": "עדיין אין כאן דבר. ההודעות חתומות בידי השולח ועוברות בקישור המוצפן הפרטי של כל חבר.",
+ "group.noVerified": "עדיין אין צ'אטים מאומתים. פתחו צ'אט 1:1 והשוו קודם את קוד הבטיחות שלו - קבוצה נבנית מחיבורים שכבר בדקתם.",
+ "group.noMoreToAdd": "אין צ'אטים מאומתים נוספים להוספה. פתחו צ'אט 1:1 והשוו קודם את קוד הבטיחות שלו.",
+ "group.directLink": "קישור ישיר עמית־לעמית",
+ "group.noDirectLink": "עדיין אין קישור ישיר - חבר אחר מעביר את ההודעות בזמן שנבנה קישור",
+ "group.relayNote": "לחלק מהחברים עדיין אין קישור ישיר אליכם. ההודעות שלהם עוברות דרך חבר אחר, שיכול לראות שאתם מדברים אך לא לקרוא מעבר לחתימה או לשנות את מה שאמרתם. הקבוצה ממשיכה לנסות לחבר אותם ישירות.",
+ "group.relayOnlyOff": "מצב ריליי בלבד כבוי, ולכן כל חבר מתחבר אליכם ישירות ולומד את כתובת ה-IP שלכם - כולל חברים שמישהו אחר הזמין. הפעילו אותו בהגדרות הרשת אם זה משנה כאן.",
+ "group.memberOffline": "{name} לא מחובר ולא יקבל הודעות. הם עדיין חברים - הסרתם מייצרת מפתחות חדשים לקבוצה.",
+ "group.joinNote": "שאר החברים ידעו שאתם בקבוצה. אין היסטוריית הודעות להשלים - קבוצה מתחילה ריקה.",
+ "group.inviteSentNote": "הקבוצה ממשיכה לעבוד עד שיאשרו. אין להם היסטוריה להשלים - הם יראו רק מה שיישלח מעכשיו.",
+ "group.errNotCreated": "הקבוצה לא נוצרה",
+ "group.errNobodyAccepted": "איש לא אישר את ההזמנה בזמן.",
+ "group.errNothingSent": "שום דבר לא נשלח ושום דבר לא אומת. סגרו וניסו שוב כשכולם מחוברים.",
+ "group.errNotFormed": "לא ניתן היה להקים את הקבוצה הזו.",
+ "group.errFull": "הקבוצה הזו מלאה.",
+ "group.errInviteFailed": "ההזמנה לא נשלחה - הצ'אט הזה אינו מחובר.",
+ "group.errNoMemberList": "בעל הקבוצה מעולם לא שלח את רשימת החברים.",
+ "group.errUnsignedList": "רשימת החברים לא נחתמה בידי בעל הקבוצה. אל תנסו שוב - הודיעו להם.",
+ "group.errNotOwner": "מישהו שאינו בעל הקבוצה ניסה לשנות את החברים.",
+ "hdr.tagline": "מוצפן מקצה לקצה",
+ "hdr.netSettings": "הגדרות רשת מתקדמות",
+ "hdr.netSettingsTitle": "הגדרות רשת מתקדמות (STUN/TURN)",
+ "hdr.disconnect": "ניתוק",
+ "status.connected": "מחובר",
+ "status.connecting": "מתחבר...",
+ "status.notConnected": "לא מחובר",
+ "status.reconnecting": "מתחבר מחדש...",
+ "status.retrying": "מנסה שוב...",
+ "status.verifying": "מאמת...",
+ "status.peerDisconnected": "העמית התנתק",
+ "sec.replayProtection": "הגנה מפני שידור חוזר",
+ "sec.messageIntegrity": "שלמות ההודעה (HMAC)",
+ "sec.forwardSecrecy": "סודיות קדימה מושלמת",
+ "sec.metadataProtection": "הגנת מטא־נתונים",
+ "sec.trafficObfuscation": "ערפול תעבורה",
+ "sec.realTests": "בדיקות קריפטוגרפיות אמיתיות",
+ "sec.simulatedData": "נתונים מדומים",
+ "sec.testPassed": "הבדיקה עברה",
+ "sec.testFailed": "הבדיקה נכשלה או אינה זמינה",
+ "sec.verificationDone": "האימות הקריפטוגרפי האמיתי הושלם",
+ "sec.verificationInProgress": "אימות אבטחה מתבצע...",
+ "sec.verificationWait": "אימות אבטחה מתבצע...\nהמתינו לסיום האימות הקריפטוגרפי בזמן אמת.",
+ "sec.verificationUnavailable": "אימות אבטחה אינו זמין",
+ "chat.placeholder": "הקלידו הודעה מוצפנת…",
+ "chat.send": "שליחת הודעה",
+ "chat.sendFiles": "שליחת קבצים",
+ "chat.hideFiles": "הסתרת קבצים",
+ "chat.recordVoice": "הקלטת הודעה קולית",
+ "chat.sendVoice": "שליחת הודעה קולית",
+ "chat.discard": "מחיקה",
+ "chat.codeBlock": "שליחה כבלוק קוד (מרחיב את שדה הקלט)",
+ "chat.codeHint": "קטע קוד · העיצוב נשמר · ⌘↵ לשליחה",
+ "chat.viewOnce": "צפייה חד־פעמית",
+ "chat.viewOnceTitle": "צפייה חד־פעמית - נעלם אחרי שהעמית קורא",
+ "chat.viewOncePrefix": "צפייה חד־פעמית · ",
+ "chat.viewOnceTap": "צפייה חד־פעמית · הקישו לחשיפה",
+ "chat.viewedOnce": "נצפה פעם אחת",
+ "chat.disappearing": "הודעה נעלמת - נמחקת בשני הצדדים",
+ "chat.disappearAfter": "היעלמות אחרי",
+ "chat.visibleFor": "גלויה למשך",
+ "chat.timerPrefix": "טיימר · ",
+ "chat.expired": "תוקף ההודעה הזו פג",
+ "chat.deleteForEveryone": "מחיקה אצל כולם",
+ "chat.sending": "שולח",
+ "chat.delivered": "נמסרה",
+ "chat.notSent": "לא נשלחה",
+ "chat.notSentReason": "לא נשלחה - הערוץ המאובטח אינו מוכן. התחברו מחדש כדי להמשיך.",
+ "chat.encrypted": "מוצפנת",
+ "chat.decrypted": "מפוענחת",
+ "chat.encryptedOnDevice": "מוצפנת במכשיר שלכם",
+ "chat.uploading": "מעלה",
+ "chat.downloading": "מוריד",
+ "chat.transferring": "מעביר…",
+ "chat.copied": "הועתק!",
+ "chat.collapse": "כיווץ",
+ "chat.closeMenu": "סגירת התפריט",
+ "chat.newChat": "צ'אט חדש",
+ "chat.newGroup": "קבוצה חדשה",
+ "chat.createGroup": "יצירת קבוצה",
+ "chat.groupChats": "צ'אטים קבוצתיים",
+ "chat.secureChat": "צ'אט מאובטח",
+ "chat.nameThis": "תנו שם לצ'אט הזה",
+ "chat.rename": "שינוי שם הצ'אט (מקומי בלבד)",
+ "chat.renameHint": "לחיצה כפולה לשינוי השם",
+ "chat.localLabel": "תווית מקומית · נשמרת רק במכשיר הזה",
+ "chat.setStatus": "קבעו את הסטטוס שלכם",
+ "chat.yourStatusPrefix": "הסטטוס שלכם - ",
+ "chat.meshHint": "עד 8 עמיתים · רשת עמית־לעמית",
+ "chat.offline": "לא מקוון",
+ "chat.noNetwork": "אין רשת · מתחבר מחדש",
+ "chat.startVoiceCall": "התחילו שיחה קולית מוצפנת",
+ "chat.startVideoCall": "התחילו שיחת וידאו מוצפנת",
+ "chat.verifyForCalls": "אמתו את ההפעלה כדי לאפשר שיחות",
+ "chat.e2eeNote": "כל הודעה מוצפנת מקצה לקצה במכשיר שלכם לפני שהיא יוצאת.",
+ "chat.peersOnly": "נשלחת מקצה לקצה רק לעמיתים מחוברים - לעולם לא נשמרת בשרת.",
+ "flow.invitationCreated": "נוצרה הזמנה מאובטחת",
+ "flow.invitationCreatedBang": "נוצרה הזמנה מאובטחת ומוצפנת!",
+ "flow.responseCreated": "נוצרה תשובה מאובטחת",
+ "flow.responseCreatedBang": "נוצרה תשובה מאובטחת!",
+ "flow.sendInvitation": "שלחו את קוד ההזמנה לבן שיחכם בערוץ מאובטח (שיחה קולית, SMS וכדומה).",
+ "flow.sendResponse": "שלחו את קוד התשובה ליוזם בערוץ מאובטח או תנו לו לסרוק את קוד ה-QR למטה.",
+ "flow.sendEncryptedCode": "שלחו את הקוד המוצפן",
+ "flow.sendTheResponse": "שלחו את התשובה",
+ "flow.pasteOrWrite": "הדביקו או כתבו קוד…",
+ "flow.processingInvitation": "מעבד את ההזמנה המאובטחת...",
+ "flow.processingResponse": "מעבד את התשובה המאובטחת...",
+ "flow.finalizing": "משלים את החיבור המאובטח...",
+ "flow.channelEstablished": "הערוץ המאובטח הוקם",
+ "flow.channelReady": "הערוץ המאובטח מוכן",
+ "flow.restoring": "משחזר את החיבור…",
+ "flow.invitationCaptured": "ההזמנה נקלטה.",
+ "err.needInvitation": "עליכם להדביק את קוד ההזמנה מבן שיחכם.",
+ "err.needResponse": "עליכם להדביק את קוד התשובה מבן שיחכם.",
+ "err.responseFormat": "מבנה התשובה אינו תקין - בדקו את הקוד",
+ "err.responseNoKey": "קוד תשובה לא תקין - מפתח קריפטוגרפי חסר או פגום. בדקו את הקוד ונסו שוב.",
+ "err.responseNoSignKey": "קוד תשובה לא תקין - חסר מפתח לאימות חתימה. בדקו את הקוד ונסו שוב.",
+ "err.responseOutdated": "נתוני התשובה מיושנים - השתמשו בהזמנה חדשה",
+ "err.setupError": "שגיאה בהקמת החיבור",
+ "err.notReady": "החיבור אינו מוכן",
+ "err.securityBreach": "זוהתה פרצת אבטחה - החיבור נדחה",
+ "err.securityValidation": "אימות האבטחה נכשל - ייתכן שזוהתה התקפה",
+ "err.retiredQr": "קוד ה-QR הזה משתמש בפורמט שהוצא משימוש ולא הצליח להעביר את ההזמנה. בקשו מהעמית ליצור חדש, או השתמשו בהעתקה והדבקה.",
+ "err.compressedQrNote": "QR דחוס עשוי להשמיט SDP לשם קיצור. השתמשו בהעתקה והדבקה אם החיבור נכשל.",
+ "err.inviteNotConnected": "ההזמנה לא נשלחה. הצ'אט הזה אינו מחובר כרגע.",
+ "err.inviteNotConnectedRetry": "ההזמנה לא נשלחה. הצ'אט הזה אינו מחובר כרגע - פתחו אותו מחדש ונסו שוב.",
+ "err.nobodyAccepted": "איש לא אישר את ההזמנה. הקבוצה לא השתנתה.",
+ "err.noAudio": "לא נקלט שמע - בדקו את הרשאת המיקרופון ונסו שוב.",
+ "err.voiceNeedsConnection": "הודעה קולית דורשת חיבור מאובטח פעיל. התחברו מחדש ונסו שוב.",
+ "err.voiceRestoring": "משחזר את החיבור - נסו לשלוח את ההודעה הקולית שוב בעוד רגע.",
+ "err.fileTooLarge": "הקובץ גדול מדי",
+ "sas.safetyNumber": "מספר בטיחות",
+ "sas.incorrect": "קוד שגוי. בדקו אותו מול העמית ונסו שוב.",
+ "sas.noMatch": "הקודים אינם תואמים",
+ "sas.makeSure": "ודאו שהקודים תואמים במדויק.",
+ "sas.tooManyAttempts": "יותר מדי ניסיונות שגויים. ההפעלה אופסה מטעמי בטיחות.",
+ "sas.verificationFailed": "האימות נכשל",
+ "sas.verified": "מאומת · סודיות קדימה מושלמת",
+ "sas.runVerification": "הפעילו אימות אבטחה",
+ "sec.panelTitle": "פרטי רשת וקריפטוגרפיה",
+ "sec.security": "אבטחה",
+ "sec.transport": "תעבורה",
+ "sec.keyExchange": "חילופי מפתחות",
+ "sec.allEnabled": "כל יכולות האבטחה מופעלות כברירת מחדל",
+ "sec.realTimeNote": "אימות בזמן אמת בעזרת פונקציות קריפטוגרפיות אמיתיות - בלי נתונים מדומים.",
+ "sec.simulatedWarning": "אזהרה: ייתכן שהחיבור אינו מלא - הערכים עשויים להיות מדומים.",
+ "dl.title": "הורדת SecureBit",
+ "dl.free": "חינם · קוד פתוח",
+ "dl.soon": "אפליקציות לנייד (iOS, אנדרואיד) והרחבות דפדפן (Chrome, Firefox, Opera) בקרוב.",
+ "chatHdr.chat": "צ'אט",
+ "chatHdr.chats": "צ'אטים",
+ "chatHdr.newChat": "+ חדש",
+ "chatHdr.secure": "מאובטח",
+ "chatHdr.p2pSub": "עמית־לעמית · מוצפן מקצה לקצה",
+ "chatHdr.save": "שמירה",
+ "chatHdr.close": "סגירה",
+ "chatHdr.expand": "הגדלה",
+ "chatHdr.you": "אתם",
+ "chatHdr.qrCode": "קוד QR",
+ "chat.disconnected": "מנותק",
+ "call.mute": "השתקה",
+ "call.endShort": "סיום",
+ "group.add": "הוספה",
+ "group.send": "שליחה",
+ "sec.pfsShort": "סודיות קדימה מושלמת",
+ "sec.cipher": "צופן",
+ "chat.close": "סגירה",
+ "file.needConnection": "העברת קבצים דורשת חיבור פתוח",
+ "presence.available": "זמין",
+ "presence.away": "מרוחק",
+ "presence.busy": "עסוק",
+ "presence.offline": "לא מקוון",
+ "presence.invisible": "בלתי נראה",
+ "presence.availableDesc": "מקוון וזמין",
+ "presence.awayDesc": "לא פעיל · יצא לרגע",
+ "presence.busyDesc": "נא לא להפריע",
+ "presence.invisibleDesc": "מופיע כלא מקוון לעמיתים",
+ "presence.online": "מקוון",
+ "conn.p2p": "עמית־לעמית · מחובר",
+ "conn.verifying": "מאמת…",
+ "conn.connecting": "מתחבר…",
+ "conn.reconnecting": "מתחבר מחדש…",
+ "conn.peerDisconnected": "העמית התנתק",
+ "conn.disconnected": "מנותק",
+ "chat.defaultLabel": "צ'אט",
+ "groupPhase.forming": "מתגבש…",
+ "groupPhase.commitments": "מחליף התחייבויות…",
+ "groupPhase.revealing": "חושף…",
+ "groupPhase.compare": "השוו את הקוד הקבוצתי",
+ "groupPhase.ready": "הקבוצה מוכנה",
+ "groupPhase.failed": "הקבוצה נכשלה",
+ "call.qualityGood": "טובה",
+ "call.qualityFair": "סבירה",
+ "call.qualityWeak": "חלשה",
+ "msg.code": "קוד",
+ "msg.timer": "טיימר",
+ "msg.voice": "קול",
+ "msg.play": "נגן",
+ "msg.sent": "נשלחה",
+ "msg.read": "נקראה",
+ "report.title": "אימות אבטחה בזמן אמת",
+ "report.active": "פעיל",
+ "report.testsPassed": "בדיקות שעברו",
+ "report.verifiedAt": "אומת בשעה",
+ "report.source": "מקור",
+ "secTest.verifyECDHKeyExchange": "חילופי מפתחות ECDH",
+ "secTest.verifyECDSASignatures": "חתימות דיגיטליות ECDSA",
+ "secTest.verifyEncryption": "הצפנת AES-GCM",
+ "secTest.verifyMessageIntegrity": "שלמות ההודעה",
+ "secTest.verifyPerfectForwardSecrecy": "סודיות קדימה מושלמת",
+ "secTest.verifyPFS": "סודיות קדימה מושלמת",
+ "secTest.verifyReplayProtection": "הגנה מפני שידור חוזר",
+ "secTest.verifyDTLSFingerprint": "טביעת אצבע DTLS",
+ "secTest.verifySASVerification": "אימות SAS",
+ "secTest.verifyMetadataProtection": "הגנת מטא־נתונים",
+ "secTest.verifyTrafficObfuscation": "ערפול תעבורה",
+ "secTest.verifyPacketPadding": "ריפוד מנות",
+ "secTest.verifyNestedEncryption": "הצפנה מקוננת",
+ "secTest.verifyNonExtractableKeys": "מפתחות שאינם ניתנים לחילוץ",
+ "secTest.verifyRateLimiting": "הגבלת קצב",
+ "secTest.verifyMutualAuth": "אימות הדדי",
+ "secTest.verifyAuthProof": "הוכחת אימות",
+ "secTest.verifyEnhancedValidation": "אימות מוגבר",
+ "secTest.verifyAdvancedFeatures": "יכולות מתקדמות",
+ "secTest.verifySignature": "בדיקת חתימה",
+ "secDetail.Security system initializing...": "מערכת האבטחה מאותחלת...",
+ "secDetail.Nested encryption active": "הצפנה מקוננת פעילה",
+ "secDetail.Nested encryption failed": "הצפנה מקוננת נכשלה",
+ "secDetail.Packet padding active": "ריפוד מנות פעיל",
+ "secDetail.Packet padding failed": "ריפוד מנות נכשל",
+ "secDetail.Advanced features active": "יכולות מתקדמות פעילות",
+ "secDetail.Advanced features failed": "יכולות מתקדמות נכשלו",
+ "secDetail.No encryption key available": "אין מפתח הצפנה זמין",
+ "secDetail.AES-GCM encryption/decryption working correctly": "הצפנה ופענוח AES-GCM פועלים כשורה",
+ "secDetail.No ECDH key pair available": "אין זוג מפתחות ECDH זמין",
+ "secDetail.Key derivation failed": "גזירת המפתח נכשלה",
+ "secDetail.No ECDSA key pair available": "אין זוג מפתחות ECDSA זמין",
+ "secDetail.ECDSA digital signatures working correctly": "חתימות דיגיטליות ECDSA פועלות כשורה",
+ "secDetail.MAC key not available or invalid": "מפתח MAC אינו זמין או אינו תקין",
+ "secDetail.Rate limiter did not block a message over the limit": "מגביל הקצב לא חסם הודעה שחרגה מהמגבלה",
+ "secDetail.Rate limiter is not available": "מגביל הקצב אינו זמין",
+ "secDetail.Replay protection is working correctly": "ההגנה מפני שידור חוזר פועלת כשורה",
+ "secDetail.Replay protection not enabled": "ההגנה מפני שידור חוזר אינה מופעלת",
+ "secDetail.SAS code not available": "קוד SAS אינו זמין",
+ "secDetail.SAS verification code is valid and available": "קוד אימות SAS תקין וזמין",
+ "secDetail.Traffic obfuscation is working correctly": "ערפול התעבורה פועל כשורה",
+ "secDetail.Traffic obfuscation not enabled": "ערפול התעבורה אינו מופעל",
+ "secDetail.No non-extractable ephemeral ECDH key pair for this session": "אין זוג מפתחות ECDH זמני ובלתי ניתן לחילוץ להפעלה הזו",
+ "secDetail.Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation": "סודיות קדימה ברמת ההפעלה בלבד: המפתחות זמניים לכל הפעלה, אך ה-Double Ratchet אינו פעיל בחיבור הזה (העמית בגרסה ישנה), ולכן מפתח הפעלה שנחשף חושף את כל השיחה",
+ "msg.pause": "השהיה",
+ "msg.failed": "נכשלה",
+ "groupErr.timeout": "חבר הפסיק להגיב לפני שהקוד היה מוכן.",
+ "groupErr.keyMismatch": "המפתח של חבר לא תאם את הזהות שנטענה עבורו.",
+ "groupErr.revealMismatch": "הערך שחבר חשף לא תאם למה שהתחייב אליו.",
+ "groupErr.commitChanged": "חבר שינה את ההתחייבות שלו באמצע.",
+ "groupErr.missingKey": "נכלל חבר שהמפתח שלו מעולם לא הגיע.",
+ "groupErr.outsider": "הגיע פריים ממישהו מחוץ לקבוצה.",
+ "groupErr.limit": "קבוצה מוגבלת לשמונה חברים.",
+ "groupErr.tooLarge": "ההודעה הייתה גדולה מדי לשליחה לקבוצה.",
+ "fileType.text": "טקסט רגיל",
+ "fileType.images": "תמונות",
+ "fileType.archives": "ארכיונים",
+ "fileType.voice": "הודעות קוליות",
+ "fileType.unsupported": "לא נתמך",
+ "qrScan.title": "סריקת קוד QR",
+ "qrScan.auto": "מצב אוטומטי",
+ "qrScan.reset": "איפוס",
+ "qrScan.autoScroll": "גלילה אוטומטית מופעלת",
+ "qrScan.starting": "מפעיל את המצלמה...",
+ "qrScan.point": "כוונו את המצלמה לקוד ה-QR",
+ "qrScan.tapFocus": "הקישו על המסך למיקוד",
+ "qrScan.focusing": "ממקד...",
+ "iceErr.notArray": "רשימת השרתים חייבת להיות מערך",
+ "iceErr.invalidJson": "JSON לא תקין",
+ "iceErr.tooMany": "יותר מדי שרתים (מקסימום {max})",
+ "iceErr.invalidEntry": "{label}: רשומה לא תקינה",
+ "iceErr.urlCount": "{label}: נדרשות בין 1 ל-{max} כתובות",
+ "iceErr.turnCreds": "{label}: שרתי TURN דורשים בדרך כלל שם משתמש ואישור",
+ "iceUrl.notString": "הכתובת חייבת להיות מחרוזת",
+ "iceUrl.empty": "הכתובת ריקה",
+ "iceUrl.tooLong": "הכתובת ארוכה מדי",
+ "iceUrl.badChars": "הכתובת מכילה תווים לא תקינים",
+ "iceUrl.badScheme": "הכתובת חייבת להתחיל ב-stun:, stuns:, turn: או turns:",
+ "iceUrl.badQuery": "לכתובת יש שאילתה לא תקינה",
+ "iceUrl.noHost": "לכתובת חסר מארח",
+ "iceUrl.badHost": "לכתובת יש מארח או פורט לא תקין",
+ "iceUrl.badTransport": "שאילתת הכתובת חייבת להיות transport=udp או transport=tcp",
+ "msg.off": "כבוי",
+ "file.consentUnavailable": "הסכמת המשתמש אינה זמינה",
+ "secLevel.MAXIMUM": "מרבי",
+ "secLevel.INITIALIZING": "מאתחל",
+ "secLevel.ERROR": "שגיאה",
+ "secLevel.UNKNOWN": "לא ידוע",
+ "status.error": "שגיאה",
+ "offline.disconnect": "ניתוק",
+ "offline.learnMore": "מידע נוסף",
+ "pwa.installApp": "התקנת האפליקציה",
+ "chat.onWeb": "אתם בגרסת הווב"
+ },
+ "fa": {
+ "language.label": "زبان",
+ "community.title": "به آیندهٔ حریم خصوصی بپیوندید",
+ "community.description": "SecureBit بهلطف جامعهاش رشد میکند. ایدهها و بازخوردهای شما آیندهٔ ارتباط امن را شکل میدهد - ساختهشده در فضای باز، با اعتبارسنجی کامل ASN.1 از یک سر تا سر دیگر.",
+ "community.github": "مخزن GitHub",
+ "community.feedback": "بازخورد",
+ "unique.eyebrow": "آنچه ما را متمایز میکند",
+ "unique.heading": "چرا SecureBit بیهمتاست",
+ "unique.s1.titleTop": "هستهٔ رمزنگاری",
+ "unique.s1.titleBottom": "چندلایه",
+ "unique.s1.collapsed": "هستهٔ رمزنگاری",
+ "unique.s1.desc": "تبادل کلید ECDH P-384، بارهای AES-256-GCM، امضاهای ECDSA و اعتبارسنجی کامل ASN.1 - همه در یک خط لولهٔ مقاوم.",
+ "unique.s1.tags": [
+ "ECDH P-384",
+ "AES-256-GCM",
+ "ECDSA",
+ "ASN.1"
+ ],
+ "unique.s2.titleTop": "همتابههمتای ناب",
+ "unique.s2.titleBottom": "روی WebRTC",
+ "unique.s2.collapsed": "WebRTC همتابههمتای ناب",
+ "unique.s2.desc": "پیامها مستقیم میان دستگاهها روی WebRTC میروند. هیچ بازپخشی دادههای شما را نگه نمیدارد - سرور فقط کمک میکند دو همتا یکدیگر را بیابند.",
+ "unique.s2.tags": [
+ "DTLS 1.3",
+ "بدون بازپخش"
+ ],
+ "unique.s3.titleTop": "رازداری پیشرو",
+ "unique.s3.titleBottom": "کامل",
+ "unique.s3.collapsed": "رازداری پیشرو",
+ "unique.s3.desc": "کلیدهای نشست پیوسته میچرخند و پس از استفاده دور ریخته میشوند، پس یک کلید لورفته هرگز گفتگوهای گذشته را باز نمیکند.",
+ "unique.s3.tags": [
+ "کلیدهای زودگذر",
+ "چرخش خودکار"
+ ],
+ "unique.s4.titleTop": "پنهانسازی",
+ "unique.s4.titleBottom": "ترافیک",
+ "unique.s4.collapsed": "پنهانسازی ترافیک",
+ "unique.s4.desc": "اندازه و زمانبندی بستهها لاییگذاری و تصادفی میشود و الگوهای فراداده را از دید هرکس که خط را میپاید پنهان میکند.",
+ "unique.s4.tags": [
+ "لاییگذاری بسته",
+ "لرزش زمانی"
+ ],
+ "unique.s5.titleTop": "صفر",
+ "unique.s5.titleBottom": "جمعآوری داده",
+ "unique.s5.collapsed": "صفر جمعآوری داده",
+ "unique.s5.desc": "بدون حساب، بدون گزارش، بدون ذخیرهٔ پیام. چیزی روی سرور نیست که نشت کند، احضار شود یا فروخته شود.",
+ "unique.s5.tags": [
+ "بدون حساب",
+ "بدون گزارش"
+ ],
+ "partners.eyebrow": "شرکا و زیستبوم",
+ "partners.heading": "مورد اعتماد شرکای ما",
+ "partners.aegis.desc": "شریک سرمایهای که ارتباطات مالی محرمانه را در سراسر سبد خود ایمن میکند.",
+ "partners.aegis.role": "پشتیبان راهبردی",
+ "partners.furilabs.desc": "گوشیهای لینوکسی با اولویت حریم خصوصی که SecureBit را بهعنوان کانال امن پیشفرض عرضه میکنند.",
+ "partners.furilabs.role": "شریک فناوری",
+ "partners.inviteTitle": "شریک شوید",
+ "partners.inviteDesc": "سختافزار یا زیرساخت حریم خصوصی میسازید؟ بیایید SecureBit را یکپارچه کنیم.",
+ "partners.inviteCta": "گفتگو را آغاز کنید",
+ "roadmap.eyebrow": "نقشهٔ راه توسعه",
+ "roadmap.heading": "تکامل SecureBit",
+ "roadmap.subheading": "از نخستین نمونهٔ اولیه تا شبکهای غیرمتمرکز و مقاوم در برابر کوانتوم - با اعتبارسنجی کامل ASN.1 در هر لایه.",
+ "roadmap.progress": "{shipped} از {total} نقطهٔ عطف انجام شد",
+ "roadmap.upcoming": "{upcoming} در راه است",
+ "roadmap.keyFeatures": "ویژگیهای کلیدی",
+ "roadmap.status.released": "منتشر شد",
+ "roadmap.status.current": "کنونی",
+ "roadmap.status.dev": "در دست توسعه",
+ "roadmap.status.planned": "برنامهریزیشده",
+ "roadmap.status.research": "پژوهش",
+ "hero.headlineTop": "خطی مستقیم",
+ "hero.headlineBottom": "که فقط شما دو نفر میخوانید.",
+ "hero.subheading": "کلیدها روی دستگاه شما ساخته و همتابههمتا مبادله میشوند. بدون حساب، بدون سروری که پیامهایتان را نگه دارد.",
+ "hero.diagramAlt": "یک خط رمزنگاریشدهٔ مستقیم به یک همتا، در حالی که دو همتای دیگر به شبکه میپیوندند",
+ "intro.createTitle": "کانال تازهای بسازید",
+ "intro.createDesc": "دستگاه شما کلیدها و یک دعوتنامهٔ یکبارمصرف میسازد. هیچچیز به سرور نمیرسد.",
+ "intro.createCta": "ساخت کلید و دعوتنامه",
+ "intro.joinTitle": "به کانالی بپیوندید",
+ "intro.scanTitle": "QR را با دوربین بخوانید",
+ "intro.scanSubtitle": "سریعترین راه - به صفحهٔ همتایتان بگیرید",
+ "intro.orPasteCode": "یا کد را بچسبانید",
+ "intro.pastePlaceholder": "کد دعوت را اینجا بچسبانید…",
+ "intro.connect": "اتصال",
+ "intro.connecting": "در حال پردازش…",
+ "roadmap.r1.title": "آغاز توسعه",
+ "roadmap.r1.sub": "ایده، نمونهٔ اولیه و برپایی زیرساخت",
+ "roadmap.r1.date": "اوایل ۲۰۲۵",
+ "roadmap.r1.features": [
+ "شکلگیری ایده و نیازمندیها",
+ "گزینش پشته: WebRTC، P2P، رمزنگاری",
+ "نخستین نمونههای پیامرسانی",
+ "ساخت مخزن و یکپارچهسازی پیوسته",
+ "معماری پایهٔ رمزنگاری",
+ "طراحی تجربه و رابط کاربری"
+ ],
+ "roadmap.r2.title": "انتشار آلفا",
+ "roadmap.r2.sub": "نخستین آلفای عمومی: گفتگوی پایه و تبادل کلید",
+ "roadmap.r2.date": "بهار ۲۰۲۵",
+ "roadmap.r2.features": [
+ "پیامرسانی پایهٔ همتابههمتا روی WebRTC",
+ "رمزنگاری سادهٔ سرتاسری (طرح نمایشی)",
+ "سیگنالینگ پایدار و اتصال دوباره",
+ "رابط کمینه برای آزمون",
+ "گردآوری بازخورد از آزمایندگان نخستین"
+ ],
+ "roadmap.r3.title": "سختسازی امنیتی",
+ "roadmap.r3.sub": "تقویت امنیت و انتشار شاخهٔ پایدار",
+ "roadmap.r3.date": "تابستان ۲۰۲۵",
+ "roadmap.r3.features": [
+ "پیادهسازی ECDH/ECDSA در محیط عملیاتی",
+ "رازداری پیشرو کامل و چرخش کلید",
+ "بهبود بررسیهای احراز هویت",
+ "رمزنگاری فایل و انتقال بارهای بزرگ",
+ "ممیزی فرایندهای پایهٔ رمزنگاری"
+ ],
+ "roadmap.r4.title": "مقیاسپذیری و پایداری",
+ "roadmap.r4.sub": "گسترش شبکه و بهبود پایداری",
+ "roadmap.r4.date": "پاییز ۲۰۲۵",
+ "roadmap.r4.features": [
+ "بهینهسازی اتصالهای P2P و گذر از NAT",
+ "سازوکارهای اتصال دوباره و صف پیام",
+ "کاهش مصرف باتری روی موبایل",
+ "پشتیبانی از همگامسازی چنددستگاهی",
+ "ابزارهای پایش و ثبت برای توسعهدهندگان"
+ ],
+ "roadmap.r5.title": "انتشار با اولویت حریم خصوصی",
+ "roadmap.r5.sub": "تمرکز بر حریم خصوصی: کمینهسازی فراداده",
+ "roadmap.r5.date": "زمستان ۲۰۲۵",
+ "roadmap.r5.features": [
+ "حفاظت از فراداده و کاهش اثر انگشت",
+ "آزمایش مسیریابی پیازی و DHT",
+ "گزینههایی برای اتصالهای ناشناس",
+ "آمادهسازی برای ممیزی باز کد",
+ "بهبود فرایندهای تأیید کاربر"
+ ],
+ "roadmap.r6.title": "نسخهٔ امنیت پیشرفته",
+ "roadmap.r6.sub": "رمزنگاری ۱۸ لایه در سطح نظامی با اعتبارسنجی کامل ASN.1",
+ "roadmap.r6.date": "اواخر ۲۰۲۵",
+ "roadmap.r6.features": [
+ "امنیت سهلایه: ECDH، DTLS و SAS",
+ "رمزنگاری ECDH P-384 به همراه AES-GCM ۲۵۶ بیتی",
+ "تأیید اثر انگشت DTLS",
+ "تأیید SAS (رشتهٔ کوتاه احراز هویت)",
+ "رازداری پیشرو کامل با چرخش کلید",
+ "پیشگیری پیشرفته از حملهٔ مرد میانی",
+ "اعتبارسنجی کامل ASN.1 DER",
+ "تأیید OID و نقاط منحنی",
+ "اعتبارسنجی ساختار SPKI",
+ "معماری WebRTC همتابههمتا",
+ "حفاظت از فراداده",
+ "کد ۱۰۰٪ متنباز"
+ ],
+ "roadmap.r7.title": "نسخهٔ رومیزی",
+ "roadmap.r7.sub": "برنامههای بومی برای ویندوز، مکاواس و لینوکس",
+ "roadmap.r7.date": "اوایل ۲۰۲۶",
+ "roadmap.r7.features": [
+ "برنامهٔ ویندوز (Tauri v2)",
+ "برنامهٔ macOS (Tauri v2)",
+ "پشتیبانی AppImage لینوکس (Tauri v2)",
+ "اعلانهای بیدرنگ",
+ "اتصال دوبارهٔ خودکار",
+ "همگامسازی میاندستگاهی",
+ "بهبود تجربه و رابط کاربری",
+ "پشتیبانی از فایل تا ۱۰۰ مگابایت"
+ ],
+ "roadmap.r8.title": "صدا و تماس امن",
+ "roadmap.r8.sub": "پیام صوتی و تماس صوتی و تصویری رمزنگاریشده",
+ "roadmap.r8.date": "اوایل ۲۰۲۶",
+ "roadmap.r8.features": [
+ "پیامهای صوتی با رمزنگاری سرتاسری",
+ "تماس صوتی رمزنگاریشدهٔ دونفره (WebRTC)",
+ "تماس تصویری رمزنگاریشدهٔ دونفره (WebRTC)",
+ "رازداری پیشرو کامل برای رسانهٔ زنده",
+ "جریانهای رسانهای محافظتشده با SRTP/DTLS",
+ "تأیید SAS در حین تماس",
+ "اعلان تماس و اتصال دوبارهٔ خودکار",
+ "رسانهٔ همتابههمتا با تأخیر کم"
+ ],
+ "roadmap.r9.title": "ارتباط گروهی",
+ "roadmap.r9.sub": "گفتگوی گروهی با حفظ حریم خصوصی",
+ "roadmap.r9.date": "هماکنون",
+ "roadmap.r9.features": [
+ "گفتگوی گروهی همتابههمتا تا ۸ شرکتکننده",
+ "تحویل شبکهای با بازپخش امضاشده بهعنوان جایگزین",
+ "یک کد ایمنی گروه که همه مقایسه میکنند",
+ "آیین تعهدسپسافشا در برابر حدس زدن کد",
+ "کلیدهای هویت جداگانه برای هر گروه، زودگذر بهطراحی",
+ "عضویت امضاشده با ترتیب دورهای",
+ "پیامهای امضاشده، پس رونوشت دوپاره اثباتپذیر است",
+ "بدون سرور، بدون کلید گروهی مشترک، بدون پیشینه"
+ ],
+ "roadmap.r10.title": "نسخهٔ موبایل",
+ "roadmap.r10.sub": "برنامههای بومی برای iOS و اندروید",
+ "roadmap.r10.date": "سهماههٔ دوم ۲۰۲۷",
+ "roadmap.r10.features": [
+ "برنامهٔ بومی iOS (Swift/SwiftUI)",
+ "برنامهٔ بومی اندروید (Kotlin/Jetpack Compose)",
+ "پشتیبانی PWA برای مرورگرهای موبایل",
+ "اعلانهای فوری بیدرنگ",
+ "بهینهسازی باتری",
+ "رابط بهینهشده برای موبایل",
+ "صف پیام برونخط",
+ "احراز هویت زیستسنجی"
+ ],
+ "roadmap.r11.title": "نسخهٔ مقاوم در برابر کوانتوم",
+ "roadmap.r11.sub": "حفاظت در برابر رایانههای کوانتومی",
+ "roadmap.r11.date": "سهماههٔ چهارم ۲۰۲۷",
+ "roadmap.r11.features": [
+ "رمزنگاری پساکوانتومی CRYSTALS-Kyber",
+ "امضای دیجیتال SPHINCS+",
+ "طرح ترکیبی: کلاسیک + پساکوانتومی",
+ "تبادل کلید امن در برابر کوانتوم",
+ "الگوریتمهای درهمسازی بهروز",
+ "کوچ نشستهای موجود",
+ "سازگاری با نسخهٔ ۵.x",
+ "پروتکلهای مقاوم در برابر کوانتوم"
+ ],
+ "roadmap.r12.title": "شبکهٔ غیرمتمرکز",
+ "roadmap.r12.sub": "شبکهٔ کاملاً غیرمتمرکز",
+ "roadmap.r12.date": "۲۰۲۸",
+ "roadmap.r12.features": [
+ "شبکهٔ مِش گرهها",
+ "DHT برای یافتن همتا",
+ "مسیریابی پیازی درونساخت",
+ "اقتصاد توکن و انگیزه برای گرهها",
+ "راهبری از راه DAO",
+ "همکنشپذیری با شبکههای دیگر",
+ "سازگاری چندسکویی",
+ "شبکهٔ خودترمیم"
+ ],
+ "roadmap.r13.title": "دستیار حریم خصوصی هوش مصنوعی",
+ "roadmap.r13.sub": "هوش مصنوعی برای حریم خصوصی و امنیت",
+ "roadmap.r13.date": "۲۰۲۸ به بعد",
+ "roadmap.r13.features": [
+ "تحلیل محلی تهدید با هوش مصنوعی",
+ "تشخیص خودکار حملهٔ مرد میانی",
+ "رمزنگاری سازگارشونده",
+ "توصیههای امنیتی شخصیسازیشده",
+ "یادگیری ماشین با دانش صفر",
+ "دستیار هوش مصنوعی خصوصی",
+ "امنیت پیشبینانه",
+ "حفاظت خودگردان در برابر حمله"
+ ],
+ "handshake.step1": "ساخت جفت کلید ECDH P-384",
+ "handshake.step2": "استخراج کد تأیید",
+ "handshake.step3": "تثبیت رازداری پیشرو کامل",
+ "handshake.securingTitle": "در حال ایمنسازی کانال شما",
+ "handshake.answerTitle": "در حال ساخت پاسخ شما",
+ "handshake.securingDesc": "کلیدهایی میسازیم که در برابر دستکاری تاب بیاورند.",
+ "handshake.shareTitle": "دعوتنامهتان را به اشتراک بگذارید",
+ "handshake.sendAnswerTitle": "پاسخ خود را برگردانید",
+ "handshake.shareDesc": "QR را نشان دهید یا کد را برای همتایتان بفرستید. یکبارمصرف است و بهزودی منقضی میشود.",
+ "handshake.sendAnswerDesc": "این پاسخ را به سازندهٔ کانال بدهید تا دستدادن را کامل کند.",
+ "handshake.establish": "برقراری اتصال",
+ "handshake.thenReceive": "سپس پاسخی را که همتایتان برمیگرداند دریافت کنید",
+ "handshake.pasteAnswerPlaceholder": "کد پاسخ همتا را بچسبانید…",
+ "handshake.answerSentNote": "این پاسخ را برای سازنده بفرستید و منتظر بمانید - گفتگو بهمحض اتصال او باز میشود.",
+ "handshake.qrHint": "این را باز نگه دارید تا همتایتان کد را بگیرد.",
+ "handshake.qrHintFrames": "دستدادن میان {frames} فریم پخش شده است - این را باز نگه دارید تا همتایتان همهٔ آنها را بگیرد.",
+ "verify.title": "تأیید امنیتی",
+ "verify.desc": "این کد ایمنی را از راه کانالی جداگانه (تماس صوتی / حضوری) با همتایتان مقایسه کنید، سپس آن را بنویسید تا گفتگو باز شود.",
+ "verify.enterLabel": "کد تأییدشده را وارد کنید",
+ "verify.placeholder": "کد را اینجا بنویسید",
+ "verify.confirm": "تأیید کد",
+ "verify.confirmed": "تأیید شد",
+ "verify.mismatch": "همخوان نیستند",
+ "verify.yours": "تأیید شما",
+ "verify.peer": "تأیید همتا",
+ "verify.pending": "در انتظار",
+ "verify.waiting": "در انتظار کد…",
+ "verify.verified": "کانال تأیید شد",
+ "verify.bothConfirmed": "هر دو طرف تأیید کردند. گفتگوی امن در حال باز شدن…",
+ "pwa.bannerTitle": "SecureBit.chat را نصب کنید",
+ "pwa.bannerDesc": "تجربهٔ برنامهٔ بومی با امنیت بیشتر",
+ "pwa.install": "نصب",
+ "pwa.dismiss": "بستن",
+ "pwa.close": "بستن",
+ "pwa.iosTitle": "نصب روی iOS",
+ "pwa.iosStep1": "دکمهٔ همرسانی را بزنید",
+ "pwa.iosStep1Hint": "معمولاً پایین سافاری",
+ "pwa.iosStep2": "«افزودن به صفحهٔ اصلی» را پیدا کنید",
+ "pwa.iosStep2Hint": "در منوی همرسانی پایین بروید",
+ "pwa.iosStep3": "«افزودن» را بزنید",
+ "pwa.iosStep3Hint": "برای نصب SecureBit.chat تأیید کنید",
+ "pwa.genericTitle": "نصب SecureBit",
+ "pwa.genericDesc": "هر مرورگر روش نصب خودش را دارد. گامهایی را برگزینید که با مرورگر شما میخواند.",
+ "pwa.gotIt": "متوجه شدم",
+ "pwa.installedTitle": "برنامه نصب شد!",
+ "pwa.installedIos": "برنامهٔ iOS نصب شد! از صفحهٔ اصلی بازش کنید.",
+ "pwa.installedGeneric": "SecureBit.chat اکنون روی دستگاه شماست",
+ "pwa.anytimeTitle": "هر وقت خواستید نصب کنید",
+ "pwa.anytimeDesc": "هنوز میتوانید SecureBit.chat را از منوی مرورگرتان نصب کنید تا بهترین تجربه را داشته باشید.",
+ "pwa.ok": "باشه",
+ "step.open": "گام ۱ · گشودن کانال",
+ "step.exchange": "گام ۲ · تبادل",
+ "step.verification": "گام ۳ · تأیید",
+ "action.create": "ساخت",
+ "action.join": "پیوستن",
+ "action.back": "بازگشت",
+ "action.copy": "رونوشت",
+ "action.copied": "رونوشت شد",
+ "action.scan": "خواندن",
+ "action.downloadDesktop": "دریافت برنامهٔ رومیزی",
+ "action.advancedSettings": "تنظیمات پیشرفته",
+ "cred.offerTag": "پیشنهاد · یا متن را رونوشت کنید",
+ "cred.answerTag": "پاسخ · یا متن را رونوشت کنید",
+ "cred.reveal": "برای نمایش کلیک کنید - این کد را خصوصی نگه دارید",
+ "qr.title": "خواندن کد QR",
+ "qr.subtitle": "دوربین را به QR آنها بگیرید",
+ "qr.scanning": "در حال خواندن…",
+ "qr.hint": "ثابت نگه دارید تا همهٔ بخشها گرفته شود. دسترسی به دوربین محلی است - چیزی بارگذاری نمیشود.",
+ "mesh.you": "شما",
+ "mesh.peer": "همتا · نشست ۱",
+ "mesh.joined2": "mara پیوست · +۲",
+ "mesh.joined3": "tobi پیوست · +۳",
+ "ice.title": "تنظیمات شبکه",
+ "ice.subtitle": "بهصورت محلی تنظیم میشود - هرگز با همتای شما به اشتراک گذاشته نمیشود",
+ "ice.intro": "SecureBit بهطور پیشفرض از سرورهای عمومی STUN برای مذاکرهٔ پیوند همتابههمتا بهره میبرد. اگر خودتان میزبانی میکنید، آن را به STUN/TURN خودتان بسپارید.",
+ "ice.publicTitle": "سرورهای عمومی (پیشفرض)",
+ "ice.publicDesc": "بدون پیکربندی. برای بیشتر کاربران مناسب است.",
+ "ice.customTitle": "سرورهای STUN/TURN خودم",
+ "ice.customDesc": "تا {max} سرور.",
+ "ice.turnNote": "یک بازپخش TURN نشانی IP هر دو همتا و زمانبندی ترافیک را میبیند - اما هرگز محتوای پیامها را، که سرتاسری رمزنگاریشده میمانند. ترجیح با ",
+ "ice.turnNoteTls": " (TLS) است.",
+ "ice.test": "آزمودن سرورها",
+ "ice.testing": "در حال آزمودن…",
+ "ice.relayTitle": "حالت فقط بازپخش",
+ "ice.relayBadge": "بیشترین حریم خصوصی",
+ "ice.relayDesc": "همهٔ ترافیک را از راه TURN میفرستد تا نشانی IP شما هرگز به همتا فاش نشود. نیازمند سرور TURN است.",
+ "ice.relayWarning": "حالت فقط بازپخش روشن است اما هیچ سرور TURN تنظیم نشده. اتصال نخواهد توانست آغاز شود.",
+ "ice.persist": "روی این دستگاه ذخیره کن",
+ "ice.persistDesc": "رمزنگاریشده در همین مرورگر ذخیره میشود. خاموش بگذارید تا فقط در همین نشست بهکار رود.",
+ "ice.forget": "ذخیرهشده را فراموش کن",
+ "ice.cancel": "انصراف",
+ "ice.apply": "اعمال",
+ "ice.errUnavailable": "WebRTC در این مرورگر در دسترس نیست",
+ "ice.errInvalid": "پیکربندی سرور نامعتبر است",
+ "update.title": "بهروزرسانی موجود است",
+ "update.desc": "نسخهٔ تازهتری از SecureBit یافت شد.",
+ "update.currentVersion": "نسخهٔ کنونی",
+ "update.newVersion": "نسخهٔ تازه",
+ "update.now": "اکنون بهروز کن",
+ "update.later": "بعداً",
+ "update.unknown": "نامشخص",
+ "update.stepSaving": "در حال ذخیرهٔ دادهها...",
+ "update.stepSwCaches": "در حال پاکسازی حافظهٔ نهان Service Worker...",
+ "update.stepSwUnregister": "در حال لغو ثبت Service Workerها...",
+ "update.stepBrowserCache": "در حال پاکسازی حافظهٔ نهان مرورگر...",
+ "update.stepVersion": "در حال بهروزرسانی نسخه...",
+ "update.stepReload": "در حال بارگذاری دوبارهٔ برنامه...",
+ "update.error": "خطای بهروزرسانی. لطفاً صفحه را دستی تازه کنید (Ctrl+F5 یا Cmd+Shift+R)",
+ "update.confirmSkip": "نسخهٔ تازهای موجود است. بهروزرسانی برای امنیت و پایداری توصیه میشود. بدون بهروزرسانی ادامه میدهید؟",
+ "offline.backOnline": "دوباره برخط",
+ "offline.mode": "حالت برونخط",
+ "offline.dismiss": "بستن",
+ "offline.lostTitle": "اتصال قطع شد",
+ "offline.lostDesc": "SecureBit اکنون در حالت برونخط است. برخی امکانات محدود است، اما دادههای شما امن میماند.",
+ "offline.point1": "نشست و کلیدهای شما نگه داشته میشود",
+ "offline.point2": "هیچ دادهای روی سرورها ذخیره نمیشود",
+ "offline.point3": "پیامها و فایلها هنگام اتصال دوباره همگام میشوند",
+ "offline.continue": "برونخط ادامه بده",
+ "offline.restoredTitle": "وقتی دوباره وصل شوید",
+ "offline.restoredDesc": "قطع اتصال چیزی از شما نمیگیرد. SecureBit همهچیز را محلی در صف میگذارد و لحظهای که برگردید نشست رمزنگاریشده را از سر میگیرد.",
+ "offline.r1Title": "پیامهای شما میرسد",
+ "offline.r1Desc": "هرچه در حالت برونخط نوشتید خودکار برای مخاطبتان فرستاده میشود.",
+ "offline.r2Title": "فایلها کامل منتقل میشوند",
+ "offline.r2Desc": "بارگذاریها از همانجا که ایستادند ادامه مییابد - نیازی به فرستادن دوباره نیست.",
+ "offline.r3Title": "پیامها و فایلهای آنها میرسد",
+ "offline.r3Desc": "هرچه مخاطبتان در زمان قطعی فرستاده، بهترتیب به شما میرسد.",
+ "offline.r4Title": "هیچچیز گم نمیشود",
+ "offline.r4Desc": "پس از اتصال دوباره هیچ شکافی نیست - گفتگو دقیقاً از همانجا که مکث کرد ادامه مییابد.",
+ "offline.gotIt": "متوجه شدم",
+ "qr.showTitle": "نمایش کد QR",
+ "qr.showSubtitle": "تمامصفحه · بگذارید همتایتان بخواند",
+ "qr.showSubtitleFrames": "تمامصفحه · بگذارید همتایتان هر {frames} فریم را بخواند",
+ "notify.enabledBody": "اعلانها روشن شد! برای پیامهای تازه آگاه میشوید.",
+ "notify.workingBody": "اعلانها کار میکند! برای پیامهای تازه آگاه میشوید.",
+ "pw.label": "ورود گذرواژه",
+ "pw.placeholder": "گذرواژه را وارد کنید...",
+ "pw.decrypt": "رمزگشایی",
+ "pw.cancel": "انصراف",
+ "file.title": "انتقال فایل",
+ "file.drop": "فایلها را اینجا بکشید و رها کنید",
+ "file.dropHint": "پیش از انتقال سرتاسری رمزنگاری میشود · تا ۱۰۰ مگابایت",
+ "file.browse": "مرور دستگاه",
+ "file.incoming": "درخواست فایل ورودی",
+ "file.accept": "پذیرش",
+ "file.reject": "رد",
+ "file.download": "دریافت",
+ "file.notReady": "اتصال آماده نیست",
+ "file.tooLarge": "فایل بیش از حد بزرگ است",
+ "file.typeNotAllowed": "این نوع فایل مجاز نیست",
+ "file.maxConcurrent": "بیشترین انتقال همزمان",
+ "file.gone": "این فایل دیگر برای دریافت در دسترس نیست.",
+ "call.incoming": "تماس ورودی",
+ "call.incomingVideo": "تماس تصویری ورودی",
+ "call.encrypted": "تماس رمزنگاریشده",
+ "call.encryptedShort": "رمزنگاریشده",
+ "call.peer": "همتای امن",
+ "call.connecting": "در حال اتصال…",
+ "call.ringing": "در حال زنگ زدن…",
+ "call.accept": "پاسخ",
+ "call.decline": "رد",
+ "call.end": "پایان تماس",
+ "call.camera": "دوربین",
+ "call.cameraOff": "دوربین خاموش",
+ "call.peerCameraOff": "دوربین همتا خاموش است",
+ "call.flipCamera": "تعویض دوربین",
+ "call.addVideo": "افزودن تصویر",
+ "call.video": "تصویر",
+ "call.muted": "بیصدا",
+ "call.expand": "بزرگنمایی",
+ "call.minimize": "کوچکنمایی",
+ "call.quality": "کیفیت اتصال",
+ "call.qualityExcellent": "عالی",
+ "call.videoPrefix": "تصویری · ",
+ "call.voicePrefix": "صوتی · ",
+ "group.new": "گروه تازه",
+ "group.create": "ساخت گروه",
+ "group.join": "پیوستن به گروه",
+ "group.name": "نام گروه",
+ "group.nameTooLong": "نام گروه بیش از حد بلند است.",
+ "group.capacity": "تا {max} نفر، همتابههمتا. پیش از باز شدن گروه، همه یک کد ایمنی را مقایسه میکنند.",
+ "group.invite": "دعوت",
+ "group.inviteMore": "اعضای بیشتری دعوت کنید",
+ "group.invitePeople": "دعوت {count} نفر",
+ "group.invitation": "دعوت گروه",
+ "group.addMembers": "افزودن عضو",
+ "group.roomFor": "جا برای {remaining} نفر دیگر هست. بهمحض پیوستن آنها همه کد گروه تازهای را مقایسه میکنند.",
+ "group.membersCount": "{count} عضو",
+ "group.member": "عضو",
+ "group.members": "اعضا",
+ "group.remove": "حذف {name}",
+ "group.message": "پیام به {name}",
+ "group.leave": "خروج",
+ "group.leaveThis": "از این گروه خارج شوید",
+ "group.cancel": "لغو گروه",
+ "group.close": "بستن",
+ "group.cancelBtn": "انصراف",
+ "group.decline": "رد",
+ "group.working": "در حال کار…",
+ "group.sasTitle": "کد ایمنی گروه",
+ "group.sasEveryone": "همه این کد را میبینند",
+ "group.sasCompare": "برای گشودن این گروه، کد گروه را با هر عضو مقایسه کنید.",
+ "group.sasReadAloud": "این رقمها را بلند بخوانید برای ",
+ "group.sasWarning": "اگر حتی یک عضو کد دیگری بخواند، کسی میان شما نشسته است. گروه را لغو کنید - تأیید نکنید.",
+ "group.confirmFirst": "نخست کد گروه را تأیید کنید",
+ "group.codeSuffix": " · کد {code}",
+ "group.exchangingNonces": "در حال تبادل مقادیر یکبارمصرف…",
+ "group.waitingCommit": "در انتظار تعهد همهٔ اعضا…",
+ "group.waitingJoin": "در انتظار پیوستن دیگر اعضا…",
+ "group.emptyChat": "هنوز چیزی اینجا نیست. پیامها را فرستنده امضا میکند و از راه پیوند رمزنگاریشدهٔ خود هر عضو میروند.",
+ "group.noVerified": "هنوز گفتگوی تأییدشدهای نیست. نخست یک گفتگوی دونفره باز کنید و کد ایمنیاش را مقایسه کنید - گروه از اتصالهایی ساخته میشود که پیشتر وارسی کردهاید.",
+ "group.noMoreToAdd": "گفتگوی تأییدشدهٔ دیگری برای افزودن نیست. نخست یک گفتگوی دونفره باز کنید و کد ایمنیاش را مقایسه کنید.",
+ "group.directLink": "پیوند مستقیم همتابههمتا",
+ "group.noDirectLink": "هنوز پیوند مستقیمی نیست - تا ساخته شود، عضو دیگری پیامها را بازپخش میکند",
+ "group.relayNote": "برخی اعضا هنوز پیوند مستقیمی با شما ندارند. پیامهایشان از راه عضو دیگری میگذرد که میبیند شما در حال گفتگویید اما نمیتواند فراتر از امضا را بخواند یا گفتهٔ شما را تغییر دهد. گروه پیوسته میکوشد آنها را مستقیم وصل کند.",
+ "group.relayOnlyOff": "حالت فقط بازپخش خاموش است، پس هر عضو مستقیم به شما وصل میشود و نشانی IP شما را میداند - از جمله اعضایی که کس دیگری دعوت کرده. اگر اینجا مهم است، آن را در تنظیمات شبکه روشن کنید.",
+ "group.memberOffline": "{name} برونخط است و پیامها را دریافت نمیکند. او همچنان عضو است - حذفش کلیدهای گروه را از نو میسازد.",
+ "group.joinNote": "دیگر اعضا از حضور شما در این گروه آگاه میشوند. پیشینهٔ پیامی برای جبران نیست - گروه خالی آغاز میشود.",
+ "group.inviteSentNote": "گروه تا پذیرش آنها به کار خود ادامه میدهد. پیشینهای برای جبران ندارند - تنها آنچه از این پس فرستاده شود را میبینند.",
+ "group.errNotCreated": "گروه ساخته نشد",
+ "group.errNobodyAccepted": "هیچکس بهموقع دعوت را نپذیرفت.",
+ "group.errNothingSent": "چیزی فرستاده نشد و چیزی تأیید نشد. این را ببندید و پس از اتصال همه دوباره بکوشید.",
+ "group.errNotFormed": "این گروه شکل نگرفت.",
+ "group.errFull": "این گروه پر است.",
+ "group.errInviteFailed": "دعوت فرستاده نشد - آن گفتگو وصل نیست.",
+ "group.errNoMemberList": "مالک گروه هرگز فهرست اعضا را نفرستاد.",
+ "group.errUnsignedList": "فهرست اعضا را مالک گروه امضا نکرده بود. دوباره نکوشید - به او بگویید.",
+ "group.errNotOwner": "کسی جز مالک گروه کوشید اعضا را تغییر دهد.",
+ "hdr.tagline": "رمزنگاریشده از سر تا سر",
+ "hdr.netSettings": "تنظیمات پیشرفتهٔ شبکه",
+ "hdr.netSettingsTitle": "تنظیمات پیشرفتهٔ شبکه (STUN/TURN)",
+ "hdr.disconnect": "قطع اتصال",
+ "status.connected": "متصل",
+ "status.connecting": "در حال اتصال...",
+ "status.notConnected": "متصل نیست",
+ "status.reconnecting": "در حال اتصال دوباره...",
+ "status.retrying": "در حال تلاش دوباره...",
+ "status.verifying": "در حال تأیید...",
+ "status.peerDisconnected": "همتا قطع شد",
+ "sec.replayProtection": "حفاظت در برابر بازپخش",
+ "sec.messageIntegrity": "یکپارچگی پیام (HMAC)",
+ "sec.forwardSecrecy": "رازداری پیشرو کامل",
+ "sec.metadataProtection": "حفاظت از فراداده",
+ "sec.trafficObfuscation": "پنهانسازی ترافیک",
+ "sec.realTests": "آزمونهای واقعی رمزنگاری",
+ "sec.simulatedData": "دادهٔ شبیهسازیشده",
+ "sec.testPassed": "آزمون موفق",
+ "sec.testFailed": "آزمون ناموفق یا در دسترس نیست",
+ "sec.verificationDone": "تأیید واقعی رمزنگاری کامل شد",
+ "sec.verificationInProgress": "تأیید امنیتی در جریان است...",
+ "sec.verificationWait": "تأیید امنیتی در جریان است...\nلطفاً تا پایان تأیید بیدرنگ رمزنگاری صبر کنید.",
+ "sec.verificationUnavailable": "تأیید امنیتی در دسترس نیست",
+ "chat.placeholder": "یک پیام رمزنگاریشده بنویسید…",
+ "chat.send": "فرستادن پیام",
+ "chat.sendFiles": "فرستادن فایل",
+ "chat.hideFiles": "پنهان کردن فایلها",
+ "chat.recordVoice": "ضبط پیام صوتی",
+ "chat.sendVoice": "فرستادن پیام صوتی",
+ "chat.discard": "دور انداختن",
+ "chat.codeBlock": "بهصورت بلوک کد بفرست (ورودی را گسترده میکند)",
+ "chat.codeHint": "قطعه کد · قالببندی حفظ میشود · ⌘↵ برای فرستادن",
+ "chat.viewOnce": "یکبار دیدن",
+ "chat.viewOnceTitle": "یکبار دیدن - پس از خواندن همتا ناپدید میشود",
+ "chat.viewOncePrefix": "یکبار دیدن · ",
+ "chat.viewOnceTap": "یکبار دیدن · برای نمایش بزنید",
+ "chat.viewedOnce": "یکبار دیده شد",
+ "chat.disappearing": "پیام ناپدیدشونده - در هر دو سو پاک میشود",
+ "chat.disappearAfter": "ناپدید شدن پس از",
+ "chat.visibleFor": "نمایان به مدت",
+ "chat.timerPrefix": "زمانسنج · ",
+ "chat.expired": "این پیام منقضی شده است",
+ "chat.deleteForEveryone": "حذف برای همه",
+ "chat.sending": "در حال فرستادن",
+ "chat.delivered": "رسید",
+ "chat.notSent": "فرستاده نشد",
+ "chat.notSentReason": "فرستاده نشد - کانال امن آماده نیست. برای ادامه دوباره وصل شوید.",
+ "chat.encrypted": "رمزنگاریشده",
+ "chat.decrypted": "رمزگشاییشده",
+ "chat.encryptedOnDevice": "روی دستگاه شما رمزنگاری شد",
+ "chat.uploading": "در حال بارگذاری",
+ "chat.downloading": "در حال دریافت",
+ "chat.transferring": "در حال انتقال…",
+ "chat.copied": "رونوشت شد!",
+ "chat.collapse": "جمع کردن",
+ "chat.closeMenu": "بستن منو",
+ "chat.newChat": "گفتگوی تازه",
+ "chat.newGroup": "گروه تازه",
+ "chat.createGroup": "ساخت گروه",
+ "chat.groupChats": "گفتگوهای گروهی",
+ "chat.secureChat": "گفتگوی امن",
+ "chat.nameThis": "به این گفتگو نام بدهید",
+ "chat.rename": "تغییر نام گفتگو (فقط محلی)",
+ "chat.renameHint": "برای تغییر نام دوبار کلیک کنید",
+ "chat.localLabel": "برچسب محلی · فقط روی این دستگاه ذخیره میشود",
+ "chat.setStatus": "وضعیت خود را تعیین کنید",
+ "chat.yourStatusPrefix": "وضعیت شما - ",
+ "chat.meshHint": "تا ۸ همتا · مِش همتابههمتا",
+ "chat.offline": "برونخط",
+ "chat.noNetwork": "بدون شبکه · در حال اتصال دوباره",
+ "chat.startVoiceCall": "تماس صوتی رمزنگاریشده را آغاز کنید",
+ "chat.startVideoCall": "تماس تصویری رمزنگاریشده را آغاز کنید",
+ "chat.verifyForCalls": "برای فعال شدن تماس، نشست را تأیید کنید",
+ "chat.e2eeNote": "هر پیام پیش از خروج، روی دستگاه شما سرتاسری رمزنگاری میشود.",
+ "chat.peersOnly": "تنها سرتاسری برای همتایان متصل فرستاده میشود - هرگز روی سرور ذخیره نمیشود.",
+ "flow.invitationCreated": "دعوتنامهٔ امن ساخته شد",
+ "flow.invitationCreatedBang": "دعوتنامهٔ امن ساخته و رمزنگاری شد!",
+ "flow.responseCreated": "پاسخ امن ساخته شد",
+ "flow.responseCreatedBang": "پاسخ امن ساخته شد!",
+ "flow.sendInvitation": "کد دعوت را از راه کانالی امن (تماس صوتی، پیامک و مانند آن) برای همصحبتتان بفرستید.",
+ "flow.sendResponse": "کد پاسخ را از راه کانالی امن برای آغازگر بفرستید یا بگذارید کد QR زیر را بخواند.",
+ "flow.sendEncryptedCode": "کد رمزنگاریشده را بفرستید",
+ "flow.sendTheResponse": "پاسخ را بفرستید",
+ "flow.pasteOrWrite": "کد را بچسبانید یا بنویسید…",
+ "flow.processingInvitation": "در حال پردازش دعوتنامهٔ امن...",
+ "flow.processingResponse": "در حال پردازش پاسخ امن...",
+ "flow.finalizing": "در حال نهایی کردن اتصال امن...",
+ "flow.channelEstablished": "کانال امن برقرار شد",
+ "flow.channelReady": "کانال امن آماده است",
+ "flow.restoring": "در حال بازیابی اتصال…",
+ "flow.invitationCaptured": "دعوتنامه گرفته شد.",
+ "err.needInvitation": "باید کد دعوت را از همصحبتتان بچسبانید.",
+ "err.needResponse": "باید کد پاسخ را از همصحبتتان بچسبانید.",
+ "err.responseFormat": "قالب پاسخ نامعتبر است - کد را وارسی کنید",
+ "err.responseNoKey": "کد پاسخ نامعتبر است - کلید رمزنگاری نبود یا خراب است. کد را وارسی کنید و دوباره بکوشید.",
+ "err.responseNoSignKey": "کد پاسخ نامعتبر است - کلید تأیید امضا نبود. کد را وارسی کنید و دوباره بکوشید.",
+ "err.responseOutdated": "دادهٔ پاسخ کهنه است - از دعوتنامهٔ تازهای بهره ببرید",
+ "err.setupError": "خطا در برپایی اتصال",
+ "err.notReady": "اتصال آماده نیست",
+ "err.securityBreach": "نقض امنیتی شناسایی شد - اتصال رد شد",
+ "err.securityValidation": "اعتبارسنجی امنیتی ناموفق بود - احتمال حمله هست",
+ "err.retiredQr": "این کد QR از قالبی بازنشسته بهره میبرد که نتوانست دعوتنامه را منتقل کند. از همتایتان بخواهید کد تازهای بسازد، یا از رونوشت و چسباندن بهره ببرید.",
+ "err.compressedQrNote": "QR فشرده ممکن است برای کوتاهی SDP را حذف کند. اگر اتصال ناموفق بود از رونوشت و چسباندن بهره ببرید.",
+ "err.inviteNotConnected": "دعوتنامه فرستاده نشد. آن گفتگو هماکنون وصل نیست.",
+ "err.inviteNotConnectedRetry": "دعوتنامه فرستاده نشد. آن گفتگو هماکنون وصل نیست - دوباره بازش کنید و بکوشید.",
+ "err.nobodyAccepted": "هیچکس دعوت را نپذیرفت. گروه بیتغییر است.",
+ "err.noAudio": "صدایی ضبط نشد - اجازهٔ میکروفون را وارسی کنید و دوباره بکوشید.",
+ "err.voiceNeedsConnection": "پیام صوتی به اتصال امن فعال نیاز دارد. دوباره وصل شوید و بکوشید.",
+ "err.voiceRestoring": "در حال بازیابی اتصال - لحظهای دیگر دوباره پیام صوتی را بفرستید.",
+ "err.fileTooLarge": "فایل بیش از حد بزرگ است",
+ "sas.safetyNumber": "شمارهٔ ایمنی",
+ "sas.incorrect": "کد نادرست است. آن را با همتایتان وارسی کنید و دوباره بکوشید.",
+ "sas.noMatch": "کدها همخوان نیستند",
+ "sas.makeSure": "مطمئن شوید کدها دقیقاً یکساناند.",
+ "sas.tooManyAttempts": "تلاش نادرست بیش از حد. نشست برای ایمنی بازنشانی شد.",
+ "sas.verificationFailed": "تأیید ناموفق بود",
+ "sas.verified": "تأییدشده · رازداری پیشرو کامل",
+ "sas.runVerification": "تأیید امنیتی را اجرا کنید",
+ "sec.panelTitle": "جزئیات شبکه و رمزنگاری",
+ "sec.security": "امنیت",
+ "sec.transport": "انتقال",
+ "sec.keyExchange": "تبادل کلید",
+ "sec.allEnabled": "همهٔ امکانات امنیتی بهطور پیشفرض روشناند",
+ "sec.realTimeNote": "تأیید بیدرنگ با توابع واقعی رمزنگاری - بدون دادهٔ ساختگی.",
+ "sec.simulatedWarning": "هشدار: شاید اتصال بهطور کامل برقرار نشده باشد - مقادیر ممکن است شبیهسازیشده باشند.",
+ "dl.title": "دریافت SecureBit",
+ "dl.free": "رایگان · متنباز",
+ "dl.soon": "برنامههای موبایل (iOS، اندروید) و افزونههای مرورگر (Chrome، Firefox، Opera) بهزودی میآیند.",
+ "chatHdr.chat": "گفتگو",
+ "chatHdr.chats": "گفتگوها",
+ "chatHdr.newChat": "+ تازه",
+ "chatHdr.secure": "امن",
+ "chatHdr.p2pSub": "همتابههمتا · رمزنگاریشده از سر تا سر",
+ "chatHdr.save": "ذخیره",
+ "chatHdr.close": "بستن",
+ "chatHdr.expand": "بزرگنمایی",
+ "chatHdr.you": "شما",
+ "chatHdr.qrCode": "کد QR",
+ "chat.disconnected": "قطعشده",
+ "call.mute": "بیصدا",
+ "call.endShort": "پایان",
+ "group.add": "افزودن",
+ "group.send": "فرستادن",
+ "sec.pfsShort": "رازداری پیشرو کامل",
+ "sec.cipher": "رمز",
+ "chat.close": "بستن",
+ "file.needConnection": "انتقال فایل به اتصال باز نیاز دارد",
+ "presence.available": "در دسترس",
+ "presence.away": "دور از دسترس",
+ "presence.busy": "مشغول",
+ "presence.offline": "برونخط",
+ "presence.invisible": "نامرئی",
+ "presence.availableDesc": "برخط و در دسترس",
+ "presence.awayDesc": "بیکار · لحظهای دور شده",
+ "presence.busyDesc": "مزاحم نشوید",
+ "presence.invisibleDesc": "برای همتایان برونخط دیده میشوید",
+ "presence.online": "برخط",
+ "conn.p2p": "همتابههمتا · متصل",
+ "conn.verifying": "در حال تأیید…",
+ "conn.connecting": "در حال اتصال…",
+ "conn.reconnecting": "در حال اتصال دوباره…",
+ "conn.peerDisconnected": "همتا قطع شد",
+ "conn.disconnected": "قطعشده",
+ "chat.defaultLabel": "گفتگو",
+ "groupPhase.forming": "در حال شکلگیری…",
+ "groupPhase.commitments": "در حال تبادل تعهدها…",
+ "groupPhase.revealing": "در حال افشا…",
+ "groupPhase.compare": "کد گروه را مقایسه کنید",
+ "groupPhase.ready": "گروه آماده است",
+ "groupPhase.failed": "گروه ناموفق بود",
+ "call.qualityGood": "خوب",
+ "call.qualityFair": "متوسط",
+ "call.qualityWeak": "ضعیف",
+ "msg.code": "کد",
+ "msg.timer": "زمانسنج",
+ "msg.voice": "صدا",
+ "msg.play": "پخش",
+ "msg.sent": "فرستاده شد",
+ "msg.read": "خوانده شد",
+ "report.title": "تأیید امنیتی بیدرنگ",
+ "report.active": "فعال",
+ "report.testsPassed": "آزمونهای موفق",
+ "report.verifiedAt": "تأیید در ساعت",
+ "report.source": "منبع",
+ "secTest.verifyECDHKeyExchange": "تبادل کلید ECDH",
+ "secTest.verifyECDSASignatures": "امضای دیجیتال ECDSA",
+ "secTest.verifyEncryption": "رمزنگاری AES-GCM",
+ "secTest.verifyMessageIntegrity": "یکپارچگی پیام",
+ "secTest.verifyPerfectForwardSecrecy": "رازداری پیشرو کامل",
+ "secTest.verifyPFS": "رازداری پیشرو کامل",
+ "secTest.verifyReplayProtection": "حفاظت در برابر بازپخش",
+ "secTest.verifyDTLSFingerprint": "اثر انگشت DTLS",
+ "secTest.verifySASVerification": "تأیید SAS",
+ "secTest.verifyMetadataProtection": "حفاظت از فراداده",
+ "secTest.verifyTrafficObfuscation": "پنهانسازی ترافیک",
+ "secTest.verifyPacketPadding": "لاییگذاری بسته",
+ "secTest.verifyNestedEncryption": "رمزنگاری تودرتو",
+ "secTest.verifyNonExtractableKeys": "کلیدهای غیرقابلاستخراج",
+ "secTest.verifyRateLimiting": "محدودسازی نرخ",
+ "secTest.verifyMutualAuth": "احراز هویت دوطرفه",
+ "secTest.verifyAuthProof": "اثبات احراز هویت",
+ "secTest.verifyEnhancedValidation": "اعتبارسنجی پیشرفته",
+ "secTest.verifyAdvancedFeatures": "امکانات پیشرفته",
+ "secTest.verifySignature": "بررسی امضا",
+ "secDetail.Security system initializing...": "سامانهٔ امنیتی در حال راهاندازی...",
+ "secDetail.Nested encryption active": "رمزنگاری تودرتو فعال است",
+ "secDetail.Nested encryption failed": "رمزنگاری تودرتو ناموفق بود",
+ "secDetail.Packet padding active": "لاییگذاری بسته فعال است",
+ "secDetail.Packet padding failed": "لاییگذاری بسته ناموفق بود",
+ "secDetail.Advanced features active": "امکانات پیشرفته فعال است",
+ "secDetail.Advanced features failed": "امکانات پیشرفته ناموفق بود",
+ "secDetail.No encryption key available": "کلید رمزنگاری در دسترس نیست",
+ "secDetail.AES-GCM encryption/decryption working correctly": "رمزنگاری و رمزگشایی AES-GCM درست کار میکند",
+ "secDetail.No ECDH key pair available": "جفت کلید ECDH در دسترس نیست",
+ "secDetail.Key derivation failed": "استخراج کلید ناموفق بود",
+ "secDetail.No ECDSA key pair available": "جفت کلید ECDSA در دسترس نیست",
+ "secDetail.ECDSA digital signatures working correctly": "امضای دیجیتال ECDSA درست کار میکند",
+ "secDetail.MAC key not available or invalid": "کلید MAC در دسترس نیست یا نامعتبر است",
+ "secDetail.Rate limiter did not block a message over the limit": "محدودکنندهٔ نرخ پیامی فراتر از حد را مسدود نکرد",
+ "secDetail.Rate limiter is not available": "محدودکنندهٔ نرخ در دسترس نیست",
+ "secDetail.Replay protection is working correctly": "حفاظت در برابر بازپخش درست کار میکند",
+ "secDetail.Replay protection not enabled": "حفاظت در برابر بازپخش روشن نیست",
+ "secDetail.SAS code not available": "کد SAS در دسترس نیست",
+ "secDetail.SAS verification code is valid and available": "کد تأیید SAS معتبر و در دسترس است",
+ "secDetail.Traffic obfuscation is working correctly": "پنهانسازی ترافیک درست کار میکند",
+ "secDetail.Traffic obfuscation not enabled": "پنهانسازی ترافیک روشن نیست",
+ "secDetail.No non-extractable ephemeral ECDH key pair for this session": "برای این نشست جفت کلید ECDH زودگذر و غیرقابلاستخراجی وجود ندارد",
+ "secDetail.Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation": "رازداری پیشرو تنها در سطح نشست: کلیدها برای هر نشست زودگذرند، اما Double Ratchet در این اتصال فعال نیست (همتا روی نسخهای قدیمیتر است)، پس لو رفتن یک کلید نشست کل گفتگو را آشکار میکند",
+ "msg.pause": "مکث",
+ "msg.failed": "ناموفق",
+ "groupErr.timeout": "عضوی پیش از آماده شدن کد از پاسخدادن بازماند.",
+ "groupErr.keyMismatch": "کلید یکی از اعضا با هویتی که برایش ادعا شده بود نخواند.",
+ "groupErr.revealMismatch": "مقداری که عضوی افشا کرد با آنچه به آن متعهد شده بود نخواند.",
+ "groupErr.commitChanged": "عضوی در میانهٔ کار تعهد خود را تغییر داد.",
+ "groupErr.missingKey": "عضوی فهرست شد که کلیدش هرگز نرسید.",
+ "groupErr.outsider": "فریمی از کسی بیرون گروه رسید.",
+ "groupErr.limit": "گروه به هشت عضو محدود است.",
+ "groupErr.tooLarge": "پیام برای فرستادن به گروه بیش از حد بزرگ بود.",
+ "fileType.text": "متن ساده",
+ "fileType.images": "تصاویر",
+ "fileType.archives": "بایگانیها",
+ "fileType.voice": "پیامهای صوتی",
+ "fileType.unsupported": "پشتیبانینشده",
+ "qrScan.title": "خواندن کد QR",
+ "qrScan.auto": "حالت خودکار",
+ "qrScan.reset": "بازنشانی",
+ "qrScan.autoScroll": "پیمایش خودکار روشن است",
+ "qrScan.starting": "در حال روشن کردن دوربین...",
+ "qrScan.point": "دوربین را به کد QR بگیرید",
+ "qrScan.tapFocus": "برای تمرکز روی صفحه بزنید",
+ "qrScan.focusing": "در حال تمرکز...",
+ "iceErr.notArray": "فهرست سرورها باید آرایه باشد",
+ "iceErr.invalidJson": "JSON نامعتبر",
+ "iceErr.tooMany": "سرورهای بیش از حد (بیشینه {max})",
+ "iceErr.invalidEntry": "{label}: مدخل نامعتبر",
+ "iceErr.urlCount": "{label}: میان ۱ تا {max} نشانی لازم است",
+ "iceErr.turnCreds": "{label}: سرورهای TURN معمولاً به نام کاربری و گواهی نیاز دارند",
+ "iceUrl.notString": "نشانی باید رشته باشد",
+ "iceUrl.empty": "نشانی خالی است",
+ "iceUrl.tooLong": "نشانی بیش از حد بلند است",
+ "iceUrl.badChars": "نشانی نویسههای نامعتبر دارد",
+ "iceUrl.badScheme": "نشانی باید با stun:، stuns:، turn: یا turns: آغاز شود",
+ "iceUrl.badQuery": "پرسوجوی نشانی نامعتبر است",
+ "iceUrl.noHost": "نشانی میزبان ندارد",
+ "iceUrl.badHost": "میزبان یا درگاه نشانی نامعتبر است",
+ "iceUrl.badTransport": "پرسوجوی نشانی باید transport=udp یا transport=tcp باشد",
+ "msg.off": "خاموش",
+ "file.consentUnavailable": "رضایت کاربر در دسترس نیست",
+ "secLevel.MAXIMUM": "بیشینه",
+ "secLevel.INITIALIZING": "در حال راهاندازی",
+ "secLevel.ERROR": "خطا",
+ "secLevel.UNKNOWN": "نامشخص",
+ "status.error": "خطا",
+ "offline.disconnect": "قطع اتصال",
+ "offline.learnMore": "بیشتر بدانید",
+ "pwa.installApp": "نصب برنامه",
+ "chat.onWeb": "شما روی نسخهٔ وب هستید"
+ },
+ "ur": {
+ "language.label": "زبان",
+ "community.title": "نجیت کے مستقبل میں شامل ہوں",
+ "community.description": "SecureBit اپنی برادری کی بدولت بڑھتا ہے۔ آپ کے خیالات اور آراء محفوظ رابطے کا مستقبل تشکیل دیتے ہیں - کھلے عام بنایا گیا، سرے سے سرے تک مکمل ASN.1 توثیق کے ساتھ۔",
+ "community.github": "GitHub مخزن",
+ "community.feedback": "رائے",
+ "unique.eyebrow": "ہمیں کیا ممتاز کرتا ہے",
+ "unique.heading": "SecureBit منفرد کیوں ہے",
+ "unique.s1.titleTop": "کثیر پرت",
+ "unique.s1.titleBottom": "خفیہ کاری کا مرکز",
+ "unique.s1.collapsed": "خفیہ کاری کا مرکز",
+ "unique.s1.desc": "ECDH P-384 کلید تبادلہ، AES-256-GCM ڈیٹا، ECDSA دستخط اور مکمل ASN.1 توثیق - سب ایک مضبوط سلسلے میں جڑے ہوئے۔",
+ "unique.s1.tags": [
+ "ECDH P-384",
+ "AES-256-GCM",
+ "ECDSA",
+ "ASN.1"
+ ],
+ "unique.s2.titleTop": "خالص ہمسطح",
+ "unique.s2.titleBottom": "WebRTC",
+ "unique.s2.collapsed": "خالص ہمسطح WebRTC",
+ "unique.s2.desc": "پیغام WebRTC کے ذریعے براہِ راست آلات کے درمیان سفر کرتے ہیں۔ کوئی ریلے آپ کا ڈیٹا نہیں رکھتا - سرور صرف دو فریقوں کو ایک دوسرے تک پہنچنے میں مدد دیتا ہے۔",
+ "unique.s2.tags": [
+ "DTLS 1.3",
+ "بغیر ریلے"
+ ],
+ "unique.s3.titleTop": "مکمل فارورڈ",
+ "unique.s3.titleBottom": "سیکرسی",
+ "unique.s3.collapsed": "فارورڈ سیکرسی",
+ "unique.s3.desc": "سیشن کی کلیدیں مسلسل بدلتی ہیں اور استعمال کے بعد ضائع کر دی جاتی ہیں، اس لیے ایک بھی افشا شدہ کلید ماضی کی گفتگو نہیں کھول سکتی۔",
+ "unique.s3.tags": [
+ "عارضی کلیدیں",
+ "خودکار تبدیلی"
+ ],
+ "unique.s4.titleTop": "ٹریفک کی",
+ "unique.s4.titleBottom": "دھندلاہٹ",
+ "unique.s4.collapsed": "ٹریفک کی دھندلاہٹ",
+ "unique.s4.desc": "پیکٹ کے حجم اور اوقات میں بھرتی اور بے ترتیبی ڈالی جاتی ہے، جس سے تار پر نظر رکھنے والے سے میٹا ڈیٹا کے نمونے چھپ جاتے ہیں۔",
+ "unique.s4.tags": [
+ "پیکٹ بھرتی",
+ "وقتی جھول"
+ ],
+ "unique.s5.titleTop": "صفر",
+ "unique.s5.titleBottom": "ڈیٹا جمع کرنا",
+ "unique.s5.collapsed": "صفر ڈیٹا جمع کرنا",
+ "unique.s5.desc": "نہ اکاؤنٹ، نہ نوشتہ، نہ پیغام کا ذخیرہ۔ سرور پر ایسا کچھ نہیں جو افشا ہو، طلب کیا جائے یا بیچا جائے۔",
+ "unique.s5.tags": [
+ "بغیر اکاؤنٹ",
+ "بغیر نوشتہ"
+ ],
+ "partners.eyebrow": "شراکت دار اور ماحول",
+ "partners.heading": "ہمارے شراکت داروں کا اعتماد",
+ "partners.aegis.desc": "سرمایہ کار شراکت دار جو اپنے پورے پورٹ فولیو میں خفیہ مالی رابطے محفوظ بناتا ہے۔",
+ "partners.aegis.role": "تزویراتی معاون",
+ "partners.furilabs.desc": "نجیت کو مقدم رکھنے والے لینکس فون جو SecureBit کو طے شدہ محفوظ چینل کے طور پر پیش کرتے ہیں۔",
+ "partners.furilabs.role": "تکنیکی شراکت دار",
+ "partners.inviteTitle": "شراکت دار بنیں",
+ "partners.inviteDesc": "نجیت کا ہارڈویئر یا ڈھانچہ بنا رہے ہیں؟ آئیے SecureBit کو یکجا کریں۔",
+ "partners.inviteCta": "گفتگو شروع کریں",
+ "roadmap.eyebrow": "ترقی کا لائحہ عمل",
+ "roadmap.heading": "SecureBit کا ارتقا",
+ "roadmap.subheading": "پہلے نمونے سے لے کر کوانٹم مزاحم، غیر مرکزی نیٹ ورک تک - ہر پرت پر مکمل ASN.1 توثیق کے ساتھ۔",
+ "roadmap.progress": "{total} میں سے {shipped} سنگِ میل مکمل",
+ "roadmap.upcoming": "{upcoming} راستے میں",
+ "roadmap.keyFeatures": "نمایاں خصوصیات",
+ "roadmap.status.released": "جاری ہوا",
+ "roadmap.status.current": "موجودہ",
+ "roadmap.status.dev": "زیرِ تعمیر",
+ "roadmap.status.planned": "منصوبہ بند",
+ "roadmap.status.research": "تحقیق",
+ "hero.headlineTop": "ایک سیدھی لکیر",
+ "hero.headlineBottom": "جسے صرف آپ دونوں پڑھ سکتے ہیں۔",
+ "hero.subheading": "کلیدیں آپ کے آلے پر بنتی ہیں اور ہمسطح تبادلہ ہوتا ہے۔ نہ اکاؤنٹ، نہ کوئی سرور جو آپ کے پیغام محفوظ کرے۔",
+ "hero.diagramAlt": "ایک فریق تک براہِ راست خفیہ لکیر، جبکہ دو مزید فریق جال میں شامل ہو رہے ہیں",
+ "intro.createTitle": "نیا چینل بنائیں",
+ "intro.createDesc": "آپ کا آلہ کلیدیں اور ایک بار کی دعوت بناتا ہے۔ کچھ بھی سرور کو نہیں چھوتا۔",
+ "intro.createCta": "کلیدیں اور دعوت بنائیں",
+ "intro.joinTitle": "چینل میں شامل ہوں",
+ "intro.scanTitle": "کیمرے سے QR پڑھیں",
+ "intro.scanSubtitle": "سب سے تیز - ساتھی کی سکرین کی طرف کریں",
+ "intro.orPasteCode": "یا کوڈ چسپاں کریں",
+ "intro.pastePlaceholder": "دعوتی کوڈ یہاں چسپاں کریں…",
+ "intro.connect": "جوڑیں",
+ "intro.connecting": "کارروائی جاری…",
+ "roadmap.r1.title": "ترقی کا آغاز",
+ "roadmap.r1.sub": "خیال، نمونہ اور ڈھانچے کی تیاری",
+ "roadmap.r1.date": "2025 کا آغاز",
+ "roadmap.r1.features": [
+ "تصور اور ضروریات کی تشکیل",
+ "انتخاب: WebRTC، P2P، خفیہ کاری",
+ "پیغام رسانی کے پہلے نمونے",
+ "مخزن کی تخلیق اور مسلسل انضمام",
+ "بنیادی خفیہ کاری کا ڈھانچہ",
+ "تجربہ اور خاکہ کی ڈیزائن"
+ ],
+ "roadmap.r2.title": "الفا اجرا",
+ "roadmap.r2.sub": "پہلا عوامی الفا: بنیادی گفتگو اور کلید تبادلہ",
+ "roadmap.r2.date": "بہار 2025",
+ "roadmap.r2.features": [
+ "WebRTC پر بنیادی ہمسطح پیغام رسانی",
+ "سادہ سرے سے سرے خفیہ کاری (نمائشی خاکہ)",
+ "مستحکم اشارہ رسانی اور دوبارہ اتصال",
+ "جانچ کے لیے کم سے کم خاکہ",
+ "ابتدائی جانچ کاروں سے رائے جمع کرنا"
+ ],
+ "roadmap.r3.title": "سلامتی کی مضبوطی",
+ "roadmap.r3.sub": "سلامتی کی تقویت اور مستحکم شاخ کا اجرا",
+ "roadmap.r3.date": "گرما 2025",
+ "roadmap.r3.features": [
+ "ECDH/ECDSA کا عملی نفاذ",
+ "مکمل فارورڈ سیکرسی اور کلید کی تبدیلی",
+ "توثیقی جانچ میں بہتری",
+ "فائل خفیہ کاری اور بڑے ڈیٹا کی منتقلی",
+ "بنیادی خفیہ عمل کا جائزہ"
+ ],
+ "roadmap.r4.title": "وسعت اور استحکام",
+ "roadmap.r4.sub": "نیٹ ورک کی وسعت اور استحکام میں بہتری",
+ "roadmap.r4.date": "خزاں 2025",
+ "roadmap.r4.features": [
+ "P2P اتصال اور NAT گزر کی بہتری",
+ "دوبارہ اتصال کے طریقے اور پیغام قطاریں",
+ "موبائل پر بیٹری کی کم کھپت",
+ "کئی آلات کے درمیان ہم آہنگی",
+ "ڈویلپرز کے لیے نگرانی اور نوشتہ اوزار"
+ ],
+ "roadmap.r5.title": "نجیت سب سے پہلے",
+ "roadmap.r5.sub": "نجیت پر توجہ: میٹا ڈیٹا کم سے کم",
+ "roadmap.r5.date": "سرما 2025",
+ "roadmap.r5.features": [
+ "میٹا ڈیٹا کا تحفظ اور شناخت میں کمی",
+ "پیازی راستہ بندی اور DHT کے تجربات",
+ "گمنام اتصال کے اختیارات",
+ "کھلے کوڈ جائزے کی تیاری",
+ "صارف کی تصدیق کے عمل میں بہتری"
+ ],
+ "roadmap.r6.title": "بڑھی ہوئی سلامتی کا ایڈیشن",
+ "roadmap.r6.sub": "مکمل ASN.1 توثیق کے ساتھ 18 پرتوں کی فوجی درجے کی خفیہ کاری",
+ "roadmap.r6.date": "2025 کے اواخر",
+ "roadmap.r6.features": [
+ "ECDH + DTLS + SAS تہری سلامتی",
+ "ECDH P-384 مع AES-GCM 256 بٹ خفیہ کاری",
+ "DTLS نقشِ انگشت کی تصدیق",
+ "SAS (مختصر توثیقی سلسلہ) تصدیق",
+ "کلید تبدیلی کے ساتھ مکمل فارورڈ سیکرسی",
+ "درمیانی حملے سے بہتر تحفظ",
+ "مکمل ASN.1 DER توثیق",
+ "OID اور EC نقاط کی تصدیق",
+ "SPKI ساخت کی توثیق",
+ "ہمسطح WebRTC ڈھانچہ",
+ "میٹا ڈیٹا کا تحفظ",
+ "100% کھلا ماخذ کوڈ"
+ ],
+ "roadmap.r7.title": "ڈیسک ٹاپ ایڈیشن",
+ "roadmap.r7.sub": "ونڈوز، میک او ایس اور لینکس کے لیے مقامی ایپلی کیشنز",
+ "roadmap.r7.date": "2026 کا آغاز",
+ "roadmap.r7.features": [
+ "ونڈوز ایپ (Tauri v2)",
+ "macOS ایپ (Tauri v2)",
+ "لینکس AppImage معاونت (Tauri v2)",
+ "فوری اطلاعات",
+ "خودکار دوبارہ اتصال",
+ "آلات کے درمیان ہم آہنگی",
+ "بہتر تجربہ اور خاکہ",
+ "100MB تک فائلوں کی معاونت"
+ ],
+ "roadmap.r8.title": "محفوظ آواز اور کالیں",
+ "roadmap.r8.sub": "خفیہ صوتی پیغام، آڈیو کال اور ویڈیو کال",
+ "roadmap.r8.date": "2026 کا آغاز",
+ "roadmap.r8.features": [
+ "سرے سے سرے تک خفیہ صوتی پیغام",
+ "1:1 خفیہ آڈیو کال (WebRTC)",
+ "1:1 خفیہ ویڈیو کال (WebRTC)",
+ "براہِ راست میڈیا کے لیے مکمل فارورڈ سیکرسی",
+ "SRTP/DTLS سے محفوظ میڈیا بہاؤ",
+ "کال کے دوران SAS تصدیق",
+ "کال اطلاعات اور خودکار دوبارہ اتصال",
+ "کم تاخیر والا ہمسطح میڈیا"
+ ],
+ "roadmap.r9.title": "گروہی رابطہ",
+ "roadmap.r9.sub": "نجیت برقرار رکھتے ہوئے گروہی گفتگو",
+ "roadmap.r9.date": "ابھی",
+ "roadmap.r9.features": [
+ "8 شرکاء تک ہمسطح گروہی گفتگو",
+ "دستخط شدہ ریلے متبادل کے ساتھ جال ترسیل",
+ "ایک گروہی حفاظتی کوڈ، جسے سب موازنہ کرتے ہیں",
+ "کوڈ کے اندازے کے خلاف عہدپھرافشا کی رسم",
+ "ہر گروہ کے لیے الگ شناختی کلیدیں، بنیادی طور پر عارضی",
+ "دور کی ترتیب کے ساتھ دستخط شدہ رکنیت",
+ "دستخط شدہ پیغام، تاکہ منقسم متن ثابت کیا جا سکے",
+ "نہ سرور، نہ مشترکہ گروہی کلید، نہ تاریخ"
+ ],
+ "roadmap.r10.title": "موبائل ایڈیشن",
+ "roadmap.r10.sub": "iOS اور اینڈرائیڈ کے لیے مقامی ایپلی کیشنز",
+ "roadmap.r10.date": "دوسری سہ ماہی 2027",
+ "roadmap.r10.features": [
+ "iOS مقامی ایپ (Swift/SwiftUI)",
+ "اینڈرائیڈ مقامی ایپ (Kotlin/Jetpack Compose)",
+ "موبائل براؤزرز کے لیے PWA معاونت",
+ "فوری پش اطلاعات",
+ "بیٹری کی بہتری",
+ "موبائل کے لیے بہتر خاکہ",
+ "آف لائن پیغام قطار",
+ "حیاتیاتی توثیق"
+ ],
+ "roadmap.r11.title": "کوانٹم مزاحم ایڈیشن",
+ "roadmap.r11.sub": "کوانٹم کمپیوٹرز سے تحفظ",
+ "roadmap.r11.date": "چوتھی سہ ماہی 2027",
+ "roadmap.r11.features": [
+ "مابعد کوانٹم خفیہ کاری CRYSTALS-Kyber",
+ "SPHINCS+ ڈیجیٹل دستخط",
+ "مخلوط خاکہ: کلاسیکی + مابعد کوانٹم",
+ "کوانٹم محفوظ کلید تبادلہ",
+ "تازہ کردہ ہیشنگ الگورتھم",
+ "موجودہ سیشنز کی منتقلی",
+ "v5.x کے ساتھ مطابقت",
+ "کوانٹم مزاحم پروٹوکول"
+ ],
+ "roadmap.r12.title": "غیر مرکزی نیٹ ورک",
+ "roadmap.r12.sub": "مکمل غیر مرکزی نیٹ ورک",
+ "roadmap.r12.date": "2028",
+ "roadmap.r12.features": [
+ "نوڈ جال نیٹ ورک",
+ "ساتھی تلاش کے لیے DHT",
+ "اندرونی پیازی راستہ بندی",
+ "ٹوکن معیشت اور نوڈ ترغیبات",
+ "DAO کے ذریعے نظم",
+ "دوسرے نیٹ ورکس کے ساتھ ہم آہنگی",
+ "کثیر پلیٹ فارم مطابقت",
+ "خود مرمت کرنے والا نیٹ ورک"
+ ],
+ "roadmap.r13.title": "AI نجیت معاون",
+ "roadmap.r13.sub": "نجیت اور سلامتی کے لیے مصنوعی ذہانت",
+ "roadmap.r13.date": "2028 اور اس کے بعد",
+ "roadmap.r13.features": [
+ "مقامی AI خطرے کا تجزیہ",
+ "درمیانی حملے کی خودکار شناخت",
+ "موافقت پذیر خفیہ کاری",
+ "ذاتی نوعیت کی حفاظتی تجاویز",
+ "صفر علم مشین لرننگ",
+ "نجی AI معاون",
+ "پیش گوئی کرنے والی سلامتی",
+ "حملوں سے خودمختار تحفظ"
+ ],
+ "handshake.step1": "ECDH P-384 کلید جوڑا بنایا جا رہا ہے",
+ "handshake.step2": "تصدیقی کوڈ اخذ کیا جا رہا ہے",
+ "handshake.step3": "مکمل فارورڈ سیکرسی طے کی جا رہی ہے",
+ "handshake.securingTitle": "آپ کا چینل محفوظ کیا جا رہا ہے",
+ "handshake.answerTitle": "آپ کا جواب تیار کیا جا رہا ہے",
+ "handshake.securingDesc": "ایسی کلیدیں ڈھالی جا رہی ہیں جو چھیڑ چھاڑ برداشت کر سکیں۔",
+ "handshake.shareTitle": "اپنی دعوت بانٹیں",
+ "handshake.sendAnswerTitle": "اپنا جواب واپس بھیجیں",
+ "handshake.shareDesc": "QR دکھائیں یا کوڈ اپنے ساتھی کو بھیجیں۔ یہ ایک بار کا ہے اور جلد ختم ہو جاتا ہے۔",
+ "handshake.sendAnswerDesc": "یہ جواب چینل بنانے والے کو دیں تاکہ وہ مصافحہ مکمل کر سکے۔",
+ "handshake.establish": "اتصال قائم کریں",
+ "handshake.thenReceive": "پھر وہ جواب وصول کریں جو آپ کا ساتھی واپس بھیجتا ہے",
+ "handshake.pasteAnswerPlaceholder": "ساتھی کا جوابی کوڈ چسپاں کریں…",
+ "handshake.answerSentNote": "یہ جواب بنانے والے کو بھیجیں، پھر انتظار کریں - ان کے جڑتے ہی گفتگو کھل جائے گی۔",
+ "handshake.qrHint": "یہ اس وقت تک کھلا رکھیں جب تک ساتھی کوڈ نہ پکڑ لے۔",
+ "handshake.qrHintFrames": "مصافحہ {frames} فریموں میں بٹا ہے - یہ اس وقت تک کھلا رکھیں جب تک ساتھی سب نہ پکڑ لے۔",
+ "verify.title": "حفاظتی تصدیق",
+ "verify.desc": "یہ حفاظتی کوڈ کسی الگ ذریعے (آواز / بالمشافہ) سے اپنے ساتھی کے ساتھ ملائیں، پھر گفتگو کھولنے کے لیے لکھیں۔",
+ "verify.enterLabel": "تصدیق شدہ کوڈ درج کریں",
+ "verify.placeholder": "کوڈ یہاں لکھیں",
+ "verify.confirm": "کوڈ کی تصدیق",
+ "verify.confirmed": "تصدیق ہو گئی",
+ "verify.mismatch": "مطابقت نہیں",
+ "verify.yours": "آپ کی تصدیق",
+ "verify.peer": "ساتھی کی تصدیق",
+ "verify.pending": "زیرِ التوا",
+ "verify.waiting": "کوڈ کا انتظار…",
+ "verify.verified": "چینل تصدیق شدہ",
+ "verify.bothConfirmed": "دونوں فریقوں نے تصدیق کی۔ محفوظ گفتگو کھولی جا رہی ہے…",
+ "pwa.bannerTitle": "SecureBit.chat نصب کریں",
+ "pwa.bannerDesc": "بہتر سلامتی کے ساتھ مقامی ایپ کا تجربہ حاصل کریں",
+ "pwa.install": "نصب",
+ "pwa.dismiss": "بند کریں",
+ "pwa.close": "بند کریں",
+ "pwa.iosTitle": "iOS پر نصب کریں",
+ "pwa.iosStep1": "اشتراک کا بٹن دبائیں",
+ "pwa.iosStep1Hint": "عموماً سفاری کے نیچے",
+ "pwa.iosStep2": "«ہوم سکرین میں شامل کریں» تلاش کریں",
+ "pwa.iosStep2Hint": "اشتراک مینو میں نیچے جائیں",
+ "pwa.iosStep3": "«شامل کریں» دبائیں",
+ "pwa.iosStep3Hint": "SecureBit.chat نصب کرنے کی تصدیق کریں",
+ "pwa.genericTitle": "SecureBit نصب کریں",
+ "pwa.genericDesc": "ہر براؤزر اپنے انداز میں نصب کرتا ہے۔ وہ مراحل چنیں جو آپ کے براؤزر سے میل کھاتے ہوں۔",
+ "pwa.gotIt": "سمجھ گیا",
+ "pwa.installedTitle": "ایپ نصب ہو گئی!",
+ "pwa.installedIos": "iOS ایپ نصب ہو گئی! ہوم سکرین سے کھولیں۔",
+ "pwa.installedGeneric": "SecureBit.chat اب آپ کے آلے پر ہے",
+ "pwa.anytimeTitle": "کسی بھی وقت نصب کریں",
+ "pwa.anytimeDesc": "بہترین تجربے کے لیے آپ اب بھی SecureBit.chat اپنے براؤزر کے مینو سے نصب کر سکتے ہیں۔",
+ "pwa.ok": "ٹھیک ہے",
+ "step.open": "مرحلہ 1 · چینل کھولیں",
+ "step.exchange": "مرحلہ 2 · تبادلہ",
+ "step.verification": "مرحلہ 3 · تصدیق",
+ "action.create": "بنائیں",
+ "action.join": "شامل ہوں",
+ "action.back": "واپس",
+ "action.copy": "نقل",
+ "action.copied": "نقل ہو گیا",
+ "action.scan": "پڑھیں",
+ "action.downloadDesktop": "ڈیسک ٹاپ ایپ ڈاؤن لوڈ کریں",
+ "action.advancedSettings": "اعلیٰ ترتیبات",
+ "cred.offerTag": "پیشکش · یا متن نقل کریں",
+ "cred.answerTag": "جواب · یا متن نقل کریں",
+ "cred.reveal": "دکھانے کے لیے دبائیں - یہ کوڈ نجی رکھیں",
+ "qr.title": "QR کوڈ پڑھیں",
+ "qr.subtitle": "اپنا کیمرہ ان کے QR کی طرف کریں",
+ "qr.scanning": "پڑھا جا رہا ہے…",
+ "qr.hint": "تمام حصے پکڑے جانے تک ہاتھ ساکت رکھیں۔ کیمرے کی رسائی مقامی ہے - کچھ اپ لوڈ نہیں ہوتا۔",
+ "mesh.you": "آپ",
+ "mesh.peer": "ساتھی · سیشن 1",
+ "mesh.joined2": "mara شامل ہوئی · +2",
+ "mesh.joined3": "tobi شامل ہوا · +3",
+ "ice.title": "نیٹ ورک کی ترتیبات",
+ "ice.subtitle": "مقامی طور پر ترتیب دی جاتی ہیں - کبھی آپ کے ساتھی سے شریک نہیں کی جاتیں",
+ "ice.intro": "SecureBit طے شدہ طور پر عوامی STUN سرور استعمال کرتا ہے تاکہ ہمسطح رابطہ طے ہو سکے۔ اگر آپ خود میزبانی کرتے ہیں تو اسے اپنے STUN/TURN کی طرف موڑ دیں۔",
+ "ice.publicTitle": "عوامی سرور (طے شدہ)",
+ "ice.publicDesc": "بغیر ترتیب۔ زیادہ تر صارفین کے لیے موزوں۔",
+ "ice.customTitle": "میرے اپنے STUN/TURN سرور",
+ "ice.customDesc": "زیادہ سے زیادہ {max} سرور۔",
+ "ice.turnNote": "TURN ریلے دونوں فریقوں کا IP اور ٹریفک کا وقت دیکھتا ہے - مگر پیغام کا مواد کبھی نہیں، جو سرے سے سرے تک خفیہ رہتا ہے۔ ترجیح دیں ",
+ "ice.turnNoteTls": " (TLS)۔",
+ "ice.test": "سرور جانچیں",
+ "ice.testing": "جانچ جاری…",
+ "ice.relayTitle": "صرف ریلے موڈ",
+ "ice.relayBadge": "زیادہ سے زیادہ نجیت",
+ "ice.relayDesc": "تمام ٹریفک TURN سے گزارتا ہے تاکہ آپ کا IP کبھی ساتھی پر ظاہر نہ ہو۔ اس کے لیے TURN سرور درکار ہے۔",
+ "ice.relayWarning": "صرف ریلے موڈ چالو ہے مگر کوئی TURN سرور ترتیب نہیں دیا گیا۔ اتصال شروع نہیں ہو سکے گا۔",
+ "ice.persist": "اس آلے پر محفوظ کریں",
+ "ice.persistDesc": "اسی براؤزر میں خفیہ صورت میں محفوظ ہوتا ہے۔ صرف اس سیشن کے لیے استعمال کرنا ہو تو بند رہنے دیں۔",
+ "ice.forget": "محفوظ شدہ بھول جائیں",
+ "ice.cancel": "منسوخ",
+ "ice.apply": "لاگو کریں",
+ "ice.errUnavailable": "اس براؤزر میں WebRTC دستیاب نہیں",
+ "ice.errInvalid": "سرور کی ترتیب غلط ہے",
+ "update.title": "تازہ کاری دستیاب ہے",
+ "update.desc": "SecureBit کا نیا نسخہ ملا ہے۔",
+ "update.currentVersion": "موجودہ نسخہ",
+ "update.newVersion": "نیا نسخہ",
+ "update.now": "ابھی تازہ کریں",
+ "update.later": "بعد میں",
+ "update.unknown": "دستیاب نہیں",
+ "update.stepSaving": "ڈیٹا محفوظ کیا جا رہا ہے...",
+ "update.stepSwCaches": "Service Worker کے ذخیرے صاف کیے جا رہے ہیں...",
+ "update.stepSwUnregister": "Service Workers کا اندراج منسوخ کیا جا رہا ہے...",
+ "update.stepBrowserCache": "براؤزر کا ذخیرہ صاف کیا جا رہا ہے...",
+ "update.stepVersion": "نسخہ تازہ کیا جا رہا ہے...",
+ "update.stepReload": "ایپلی کیشن دوبارہ لوڈ ہو رہی ہے...",
+ "update.error": "تازہ کاری میں خرابی۔ براہِ کرم صفحہ خود تازہ کریں (Ctrl+F5 یا Cmd+Shift+R)",
+ "update.confirmSkip": "نیا نسخہ دستیاب ہے۔ سلامتی اور استحکام کے لیے تازہ کاری کی سفارش ہے۔ بغیر تازہ کاری جاری رکھیں؟",
+ "offline.backOnline": "دوبارہ آن لائن",
+ "offline.mode": "آف لائن موڈ",
+ "offline.dismiss": "بند کریں",
+ "offline.lostTitle": "اتصال ٹوٹ گیا",
+ "offline.lostDesc": "SecureBit اب آف لائن موڈ میں ہے۔ کچھ خصوصیات محدود ہیں، مگر آپ کا ڈیٹا محفوظ رہتا ہے۔",
+ "offline.point1": "آپ کا سیشن اور کلیدیں محفوظ ہیں",
+ "offline.point2": "کوئی ڈیٹا سرورز پر ذخیرہ نہیں ہوتا",
+ "offline.point3": "دوبارہ جڑنے پر پیغام اور فائلیں ہم آہنگ ہو جائیں گی",
+ "offline.continue": "آف لائن جاری رکھیں",
+ "offline.restoredTitle": "جب آپ دوبارہ جڑیں گے",
+ "offline.restoredDesc": "اتصال ٹوٹنے سے آپ کا کچھ نہیں جاتا۔ SecureBit سب کچھ مقامی قطار میں رکھتا ہے اور آپ کے واپس آتے ہی خفیہ سیشن بحال کر دیتا ہے۔",
+ "offline.r1Title": "آپ کے پیغام پہنچ جاتے ہیں",
+ "offline.r1Desc": "آف لائن جو کچھ آپ نے لکھا وہ خودکار طور پر آپ کے رابطے کو بھیج دیا جاتا ہے۔",
+ "offline.r2Title": "فائلوں کی منتقلی مکمل ہوتی ہے",
+ "offline.r2Desc": "اپ لوڈ وہیں سے جاری ہوتے ہیں جہاں رکے تھے - دوبارہ بھیجنے کی ضرورت نہیں۔",
+ "offline.r3Title": "ان کے پیغام اور فائلیں آ جاتی ہیں",
+ "offline.r3Desc": "بندش کے دوران آپ کے رابطے نے جو بھیجا، وہ ترتیب سے آپ تک پہنچتا ہے۔",
+ "offline.r4Title": "کچھ ضائع نہیں ہوتا",
+ "offline.r4Desc": "دوبارہ جڑنے کے بعد کوئی خلا نہیں - گفتگو بالکل وہیں سے جاری رہتی ہے جہاں رکی تھی۔",
+ "offline.gotIt": "سمجھ گیا",
+ "qr.showTitle": "QR کوڈ دکھائیں",
+ "qr.showSubtitle": "پوری سکرین · ساتھی کو پڑھنے دیں",
+ "qr.showSubtitleFrames": "پوری سکرین · ساتھی کو تمام {frames} فریم پڑھنے دیں",
+ "notify.enabledBody": "اطلاعات چالو ہو گئیں! نئے پیغامات کی اطلاع ملے گی۔",
+ "notify.workingBody": "اطلاعات کام کر رہی ہیں! نئے پیغامات کی اطلاع ملے گی۔",
+ "pw.label": "پاس ورڈ درج کریں",
+ "pw.placeholder": "پاس ورڈ درج کریں...",
+ "pw.decrypt": "کھولیں",
+ "pw.cancel": "منسوخ",
+ "file.title": "فائل منتقلی",
+ "file.drop": "فائلیں یہاں کھینچ کر چھوڑیں",
+ "file.dropHint": "منتقلی سے پہلے سرے سے سرے تک خفیہ · 100 MB تک",
+ "file.browse": "آلہ دیکھیں",
+ "file.incoming": "آنے والی فائل کی درخواست",
+ "file.accept": "قبول",
+ "file.reject": "رد",
+ "file.download": "ڈاؤن لوڈ",
+ "file.notReady": "اتصال تیار نہیں",
+ "file.tooLarge": "فائل بہت بڑی ہے",
+ "file.typeNotAllowed": "اس قسم کی فائل کی اجازت نہیں",
+ "file.maxConcurrent": "ایک ساتھ زیادہ سے زیادہ منتقلیاں",
+ "file.gone": "یہ فائل اب ڈاؤن لوڈ کے لیے دستیاب نہیں۔",
+ "call.incoming": "آنے والی کال",
+ "call.incomingVideo": "آنے والی ویڈیو کال",
+ "call.encrypted": "خفیہ کال",
+ "call.encryptedShort": "خفیہ",
+ "call.peer": "محفوظ ساتھی",
+ "call.connecting": "جوڑا جا رہا ہے…",
+ "call.ringing": "گھنٹی بج رہی ہے…",
+ "call.accept": "قبول",
+ "call.decline": "رد",
+ "call.end": "کال ختم کریں",
+ "call.camera": "کیمرہ",
+ "call.cameraOff": "کیمرہ بند",
+ "call.peerCameraOff": "ساتھی کا کیمرہ بند ہے",
+ "call.flipCamera": "کیمرہ بدلیں",
+ "call.addVideo": "ویڈیو شامل کریں",
+ "call.video": "ویڈیو",
+ "call.muted": "خاموش",
+ "call.expand": "بڑا کریں",
+ "call.minimize": "چھوٹا کریں",
+ "call.quality": "اتصال کا معیار",
+ "call.qualityExcellent": "بہترین",
+ "call.videoPrefix": "ویڈیو · ",
+ "call.voicePrefix": "آواز · ",
+ "group.new": "نیا گروہ",
+ "group.create": "گروہ بنائیں",
+ "group.join": "گروہ میں شامل ہوں",
+ "group.name": "گروہ کا نام",
+ "group.nameTooLong": "گروہ کا نام بہت لمبا ہے۔",
+ "group.capacity": "{max} افراد تک، ہمسطح۔ گروہ کھلنے سے پہلے سب ایک حفاظتی کوڈ ملائیں گے۔",
+ "group.invite": "دعوت",
+ "group.inviteMore": "مزید ارکان بلائیں",
+ "group.invitePeople": "{count} افراد کو بلائیں",
+ "group.invitation": "گروہی دعوت",
+ "group.addMembers": "ارکان شامل کریں",
+ "group.roomFor": "{remaining} مزید کی گنجائش ہے۔ ان کے شامل ہوتے ہی سب نیا گروہی کوڈ ملائیں گے۔",
+ "group.membersCount": "{count} ارکان",
+ "group.member": "رکن",
+ "group.members": "ارکان",
+ "group.remove": "{name} کو ہٹائیں",
+ "group.message": "{name} کو پیغام",
+ "group.leave": "چھوڑیں",
+ "group.leaveThis": "یہ گروہ چھوڑ دیں",
+ "group.cancel": "گروہ منسوخ کریں",
+ "group.close": "بند کریں",
+ "group.cancelBtn": "منسوخ",
+ "group.decline": "رد",
+ "group.working": "کام جاری…",
+ "group.sasTitle": "گروہی حفاظتی کوڈ",
+ "group.sasEveryone": "یہ کوڈ سب کو نظر آتا ہے",
+ "group.sasCompare": "یہ گروہ کھولنے کے لیے گروہی کوڈ ہر رکن کے ساتھ ملائیں۔",
+ "group.sasReadAloud": "یہ ہندسے بلند آواز میں پڑھ کر سنائیں ",
+ "group.sasWarning": "اگر ایک رکن بھی مختلف کوڈ پڑھے تو کوئی آپ کے درمیان بیٹھا ہے۔ گروہ منسوخ کریں - تصدیق نہ کریں۔",
+ "group.confirmFirst": "پہلے گروہی کوڈ کی تصدیق کریں",
+ "group.codeSuffix": " · کوڈ {code}",
+ "group.exchangingNonces": "یک بار اقدار کا تبادلہ…",
+ "group.waitingCommit": "ہر رکن کے عہد کا انتظار…",
+ "group.waitingJoin": "باقی ارکان کے شامل ہونے کا انتظار…",
+ "group.emptyChat": "یہاں ابھی کچھ نہیں۔ پیغام ان کے بھیجنے والے کے دستخط سے آتے ہیں اور ہر رکن کے اپنے خفیہ رابطے سے سفر کرتے ہیں۔",
+ "group.noVerified": "ابھی کوئی تصدیق شدہ گفتگو نہیں۔ پہلے ایک دو طرفہ گفتگو کھولیں اور اس کا حفاظتی کوڈ ملائیں - گروہ انہی رابطوں سے بنتا ہے جنہیں آپ پہلے جانچ چکے ہوں۔",
+ "group.noMoreToAdd": "شامل کرنے کے لیے مزید تصدیق شدہ گفتگو نہیں۔ پہلے ایک دو طرفہ گفتگو کھولیں اور اس کا حفاظتی کوڈ ملائیں۔",
+ "group.directLink": "براہِ راست ہمسطح رابطہ",
+ "group.noDirectLink": "ابھی براہِ راست رابطہ نہیں - جب تک بنے، دوسرا رکن پیغام آگے پہنچاتا ہے",
+ "group.relayNote": "کچھ ارکان کا ابھی آپ سے براہِ راست رابطہ نہیں۔ ان کے پیغام کسی دوسرے رکن سے گزرتے ہیں، جو دیکھ سکتا ہے کہ آپ بات کر رہے ہیں مگر دستخط سے آگے پڑھ نہیں سکتا اور نہ آپ کی بات بدل سکتا ہے۔ گروہ انہیں براہِ راست جوڑنے کی کوشش جاری رکھتا ہے۔",
+ "group.relayOnlyOff": "صرف ریلے موڈ بند ہے، اس لیے ہر رکن آپ سے براہِ راست جڑتا ہے اور آپ کا IP جان لیتا ہے - ان ارکان سمیت جنہیں کسی اور نے بلایا۔ اگر یہاں یہ اہم ہے تو نیٹ ورک ترتیبات میں اسے چالو کریں۔",
+ "group.memberOffline": "{name} آف لائن ہیں اور پیغام وصول نہیں کریں گے۔ وہ اب بھی رکن ہیں - انہیں ہٹانے سے گروہ کی کلیدیں نئی بنتی ہیں۔",
+ "group.joinNote": "باقی ارکان کو اس گروہ میں آپ کی موجودگی کا علم ہو جائے گا۔ پچھلے پیغامات نہیں ہیں - گروہ خالی شروع ہوتا ہے۔",
+ "group.inviteSentNote": "ان کے قبول کرنے تک گروہ کام کرتا رہتا ہے۔ ان کے لیے پچھلی تاریخ نہیں - وہ صرف وہی دیکھیں گے جو اب سے بھیجا جائے۔",
+ "group.errNotCreated": "گروہ نہیں بنا",
+ "group.errNobodyAccepted": "کسی نے وقت پر دعوت قبول نہیں کی۔",
+ "group.errNothingSent": "نہ کچھ بھیجا گیا نہ کچھ تصدیق ہوا۔ اسے بند کریں اور سب کے جڑنے پر دوبارہ کوشش کریں۔",
+ "group.errNotFormed": "یہ گروہ نہیں بن سکا۔",
+ "group.errFull": "یہ گروہ بھر چکا ہے۔",
+ "group.errInviteFailed": "دعوت نہیں بھیجی جا سکی - وہ گفتگو جڑی ہوئی نہیں۔",
+ "group.errNoMemberList": "گروہ کے مالک نے ارکان کی فہرست کبھی نہیں بھیجی۔",
+ "group.errUnsignedList": "ارکان کی فہرست پر گروہ کے مالک کے دستخط نہیں تھے۔ دوبارہ کوشش نہ کریں - انہیں بتائیں۔",
+ "group.errNotOwner": "گروہ کے مالک کے علاوہ کسی نے ارکان بدلنے کی کوشش کی۔",
+ "hdr.tagline": "سرے سے سرے تک خفیہ",
+ "hdr.netSettings": "اعلیٰ نیٹ ورک ترتیبات",
+ "hdr.netSettingsTitle": "اعلیٰ نیٹ ورک ترتیبات (STUN/TURN)",
+ "hdr.disconnect": "منقطع کریں",
+ "status.connected": "جڑا ہوا",
+ "status.connecting": "جوڑا جا رہا ہے...",
+ "status.notConnected": "جڑا ہوا نہیں",
+ "status.reconnecting": "دوبارہ جوڑا جا رہا ہے...",
+ "status.retrying": "دوبارہ کوشش...",
+ "status.verifying": "تصدیق جاری...",
+ "status.peerDisconnected": "ساتھی منقطع ہو گیا",
+ "sec.replayProtection": "بازگشت سے تحفظ",
+ "sec.messageIntegrity": "پیغام کی سالمیت (HMAC)",
+ "sec.forwardSecrecy": "مکمل فارورڈ سیکرسی",
+ "sec.metadataProtection": "میٹا ڈیٹا کا تحفظ",
+ "sec.trafficObfuscation": "ٹریفک کی دھندلاہٹ",
+ "sec.realTests": "حقیقی خفیہ نگاری کے تجربات",
+ "sec.simulatedData": "مصنوعی ڈیٹا",
+ "sec.testPassed": "تجربہ کامیاب",
+ "sec.testFailed": "تجربہ ناکام یا دستیاب نہیں",
+ "sec.verificationDone": "حقیقی خفیہ نگاری کی تصدیق مکمل ہوئی",
+ "sec.verificationInProgress": "حفاظتی تصدیق جاری ہے...",
+ "sec.verificationWait": "حفاظتی تصدیق جاری ہے...\nبراہِ کرم فوری خفیہ نگاری کی تصدیق مکمل ہونے کا انتظار کریں۔",
+ "sec.verificationUnavailable": "حفاظتی تصدیق دستیاب نہیں",
+ "chat.placeholder": "خفیہ پیغام لکھیں…",
+ "chat.send": "پیغام بھیجیں",
+ "chat.sendFiles": "فائلیں بھیجیں",
+ "chat.hideFiles": "فائلیں چھپائیں",
+ "chat.recordVoice": "صوتی پیغام ریکارڈ کریں",
+ "chat.sendVoice": "صوتی پیغام بھیجیں",
+ "chat.discard": "ضائع کریں",
+ "chat.codeBlock": "کوڈ بلاک کے طور پر بھیجیں (خانہ بڑا ہو جاتا ہے)",
+ "chat.codeHint": "کوڈ کا ٹکڑا · ترتیب محفوظ · بھیجنے کے لیے ⌘↵",
+ "chat.viewOnce": "ایک بار دیکھیں",
+ "chat.viewOnceTitle": "ایک بار دیکھیں - ساتھی کے پڑھتے ہی غائب",
+ "chat.viewOncePrefix": "ایک بار دیکھیں · ",
+ "chat.viewOnceTap": "ایک بار دیکھیں · دکھانے کے لیے دبائیں",
+ "chat.viewedOnce": "ایک بار دیکھا گیا",
+ "chat.disappearing": "غائب ہونے والا پیغام - دونوں طرف سے حذف",
+ "chat.disappearAfter": "اس کے بعد غائب",
+ "chat.visibleFor": "اتنی دیر نظر آئے گا",
+ "chat.timerPrefix": "ٹائمر · ",
+ "chat.expired": "اس پیغام کی میعاد ختم ہو چکی ہے",
+ "chat.deleteForEveryone": "سب کے لیے حذف کریں",
+ "chat.sending": "بھیجا جا رہا ہے",
+ "chat.delivered": "پہنچ گیا",
+ "chat.notSent": "نہیں بھیجا گیا",
+ "chat.notSentReason": "نہیں بھیجا گیا - محفوظ چینل تیار نہیں۔ جاری رکھنے کے لیے دوبارہ جڑیں۔",
+ "chat.encrypted": "خفیہ",
+ "chat.decrypted": "کھولا گیا",
+ "chat.encryptedOnDevice": "آپ کے آلے پر خفیہ کیا گیا",
+ "chat.uploading": "اپ لوڈ جاری",
+ "chat.downloading": "ڈاؤن لوڈ جاری",
+ "chat.transferring": "منتقلی جاری…",
+ "chat.copied": "نقل ہو گیا!",
+ "chat.collapse": "سمیٹیں",
+ "chat.closeMenu": "مینو بند کریں",
+ "chat.newChat": "نئی گفتگو",
+ "chat.newGroup": "نیا گروہ",
+ "chat.createGroup": "گروہ بنائیں",
+ "chat.groupChats": "گروہی گفتگو",
+ "chat.secureChat": "محفوظ گفتگو",
+ "chat.nameThis": "اس گفتگو کو نام دیں",
+ "chat.rename": "گفتگو کا نام بدلیں (صرف مقامی)",
+ "chat.renameHint": "نام بدلنے کے لیے دو بار دبائیں",
+ "chat.localLabel": "مقامی عنوان · صرف اسی آلے پر محفوظ",
+ "chat.setStatus": "اپنی حالت مقرر کریں",
+ "chat.yourStatusPrefix": "آپ کی حالت - ",
+ "chat.meshHint": "8 ساتھیوں تک · ہمسطح جال",
+ "chat.offline": "آف لائن",
+ "chat.noNetwork": "نیٹ ورک نہیں · دوبارہ جڑا جا رہا ہے",
+ "chat.startVoiceCall": "خفیہ صوتی کال شروع کریں",
+ "chat.startVideoCall": "خفیہ ویڈیو کال شروع کریں",
+ "chat.verifyForCalls": "کالیں چالو کرنے کے لیے سیشن کی تصدیق کریں",
+ "chat.e2eeNote": "ہر پیغام آپ کے آلے سے نکلنے سے پہلے سرے سے سرے تک خفیہ کیا جاتا ہے۔",
+ "chat.peersOnly": "صرف جڑے ہوئے ساتھیوں کو سرے سے سرے تک بھیجا جاتا ہے - کبھی سرور پر ذخیرہ نہیں ہوتا۔",
+ "flow.invitationCreated": "محفوظ دعوت بن گئی",
+ "flow.invitationCreatedBang": "محفوظ دعوت بن کر خفیہ ہو گئی!",
+ "flow.responseCreated": "محفوظ جواب بن گیا",
+ "flow.responseCreatedBang": "محفوظ جواب بن گیا!",
+ "flow.sendInvitation": "دعوتی کوڈ اپنے مخاطب کو کسی محفوظ ذریعے (صوتی کال، ایس ایم ایس وغیرہ) سے بھیجیں۔",
+ "flow.sendResponse": "جوابی کوڈ آغاز کرنے والے کو محفوظ ذریعے سے بھیجیں یا انہیں نیچے دیا QR کوڈ پڑھنے دیں۔",
+ "flow.sendEncryptedCode": "خفیہ کوڈ بھیجیں",
+ "flow.sendTheResponse": "جواب بھیجیں",
+ "flow.pasteOrWrite": "کوڈ چسپاں کریں یا لکھیں…",
+ "flow.processingInvitation": "محفوظ دعوت پر کارروائی جاری...",
+ "flow.processingResponse": "محفوظ جواب پر کارروائی جاری...",
+ "flow.finalizing": "محفوظ اتصال کو حتمی شکل دی جا رہی ہے...",
+ "flow.channelEstablished": "محفوظ چینل قائم ہو گیا",
+ "flow.channelReady": "محفوظ چینل تیار ہے",
+ "flow.restoring": "اتصال بحال کیا جا رہا ہے…",
+ "flow.invitationCaptured": "دعوت پکڑ لی گئی۔",
+ "err.needInvitation": "آپ کو اپنے مخاطب سے ملا دعوتی کوڈ چسپاں کرنا ہوگا۔",
+ "err.needResponse": "آپ کو اپنے مخاطب سے ملا جوابی کوڈ چسپاں کرنا ہوگا۔",
+ "err.responseFormat": "جواب کی ساخت غلط ہے - کوڈ جانچیں",
+ "err.responseNoKey": "جوابی کوڈ غلط ہے - خفیہ نگاری کی کلید غائب یا خراب ہے۔ کوڈ جانچ کر دوبارہ کوشش کریں۔",
+ "err.responseNoSignKey": "جوابی کوڈ غلط ہے - دستخط کی تصدیق کی کلید غائب ہے۔ کوڈ جانچ کر دوبارہ کوشش کریں۔",
+ "err.responseOutdated": "جوابی ڈیٹا پرانا ہے - براہِ کرم نئی دعوت استعمال کریں",
+ "err.setupError": "اتصال قائم کرنے میں خرابی",
+ "err.notReady": "اتصال تیار نہیں",
+ "err.securityBreach": "حفاظتی خلاف ورزی کا پتا چلا - اتصال رد کر دیا گیا",
+ "err.securityValidation": "حفاظتی توثیق ناکام - ممکنہ حملے کا پتا چلا",
+ "err.retiredQr": "یہ QR کوڈ متروک ساخت استعمال کرتا ہے جو دعوت منتقل نہ کر سکی۔ ساتھی سے نیا بنانے کو کہیں، یا نقل و چسپاں استعمال کریں۔",
+ "err.compressedQrNote": "دبایا ہوا QR اختصار کے لیے SDP چھوڑ سکتا ہے۔ اتصال ناکام ہو تو نقل و چسپاں استعمال کریں۔",
+ "err.inviteNotConnected": "دعوت نہیں بھیجی جا سکی۔ وہ گفتگو اس وقت جڑی ہوئی نہیں۔",
+ "err.inviteNotConnectedRetry": "دعوت نہیں بھیجی جا سکی۔ وہ گفتگو اس وقت جڑی ہوئی نہیں - دوبارہ کھول کر کوشش کریں۔",
+ "err.nobodyAccepted": "کسی نے دعوت قبول نہیں کی۔ گروہ جوں کا توں ہے۔",
+ "err.noAudio": "کوئی آواز ریکارڈ نہیں ہوئی - مائیکروفون کی اجازت جانچ کر دوبارہ کوشش کریں۔",
+ "err.voiceNeedsConnection": "صوتی پیغام کے لیے فعال محفوظ اتصال درکار ہے۔ دوبارہ جڑ کر کوشش کریں۔",
+ "err.voiceRestoring": "اتصال بحال ہو رہا ہے - لمحہ بھر بعد صوتی پیغام دوبارہ بھیجیں۔",
+ "err.fileTooLarge": "فائل بہت بڑی ہے",
+ "sas.safetyNumber": "حفاظتی نمبر",
+ "sas.incorrect": "کوڈ غلط ہے۔ اسے ساتھی کے ساتھ جانچ کر دوبارہ کوشش کریں۔",
+ "sas.noMatch": "کوڈ آپس میں نہیں ملتے",
+ "sas.makeSure": "یقین کر لیں کہ کوڈ بالکل ایک جیسے ہیں۔",
+ "sas.tooManyAttempts": "بہت زیادہ غلط کوششیں۔ حفاظت کے لیے سیشن دوبارہ ترتیب دیا گیا۔",
+ "sas.verificationFailed": "تصدیق ناکام",
+ "sas.verified": "تصدیق شدہ · مکمل فارورڈ سیکرسی",
+ "sas.runVerification": "حفاظتی تصدیق چلائیں",
+ "sec.panelTitle": "نیٹ ورک اور خفیہ نگاری کی تفصیل",
+ "sec.security": "سلامتی",
+ "sec.transport": "ترسیل",
+ "sec.keyExchange": "کلید تبادلہ",
+ "sec.allEnabled": "تمام حفاظتی خصوصیات طے شدہ طور پر چالو ہیں",
+ "sec.realTimeNote": "حقیقی خفیہ نگاری کے افعال سے فوری تصدیق - کوئی جعلی ڈیٹا نہیں۔",
+ "sec.simulatedWarning": "انتباہ: ممکن ہے اتصال مکمل قائم نہ ہوا ہو - اقدار مصنوعی ہو سکتی ہیں۔",
+ "dl.title": "SecureBit ڈاؤن لوڈ کریں",
+ "dl.free": "مفت · کھلا ماخذ",
+ "dl.soon": "موبائل (iOS، اینڈرائیڈ) اور براؤزر اضافے (Chrome، Firefox، Opera) جلد آ رہے ہیں۔",
+ "chatHdr.chat": "گفتگو",
+ "chatHdr.chats": "گفتگوئیں",
+ "chatHdr.newChat": "+ نئی",
+ "chatHdr.secure": "محفوظ",
+ "chatHdr.p2pSub": "ہمسطح · سرے سے سرے تک خفیہ",
+ "chatHdr.save": "محفوظ کریں",
+ "chatHdr.close": "بند کریں",
+ "chatHdr.expand": "بڑا کریں",
+ "chatHdr.you": "آپ",
+ "chatHdr.qrCode": "QR کوڈ",
+ "chat.disconnected": "منقطع",
+ "call.mute": "خاموش",
+ "call.endShort": "ختم",
+ "group.add": "شامل کریں",
+ "group.send": "بھیجیں",
+ "sec.pfsShort": "مکمل فارورڈ سیکرسی",
+ "sec.cipher": "سائفر",
+ "chat.close": "بند کریں",
+ "file.needConnection": "فائل منتقلی کے لیے کھلا اتصال درکار ہے",
+ "presence.available": "دستیاب",
+ "presence.away": "دور",
+ "presence.busy": "مصروف",
+ "presence.offline": "آف لائن",
+ "presence.invisible": "پوشیدہ",
+ "presence.availableDesc": "آن لائن اور دستیاب",
+ "presence.awayDesc": "بے کار · تھوڑی دیر کے لیے گیا",
+ "presence.busyDesc": "زحمت نہ دیں",
+ "presence.invisibleDesc": "ساتھیوں کو آف لائن دکھائی دیں",
+ "presence.online": "آن لائن",
+ "conn.p2p": "ہمسطح · جڑا ہوا",
+ "conn.verifying": "تصدیق جاری…",
+ "conn.connecting": "جوڑا جا رہا ہے…",
+ "conn.reconnecting": "دوبارہ جوڑا جا رہا ہے…",
+ "conn.peerDisconnected": "ساتھی منقطع ہو گیا",
+ "conn.disconnected": "منقطع",
+ "chat.defaultLabel": "گفتگو",
+ "groupPhase.forming": "تشکیل جاری…",
+ "groupPhase.commitments": "عہدوں کا تبادلہ…",
+ "groupPhase.revealing": "افشا جاری…",
+ "groupPhase.compare": "گروہی کوڈ ملائیں",
+ "groupPhase.ready": "گروہ تیار",
+ "groupPhase.failed": "گروہ ناکام",
+ "call.qualityGood": "اچھا",
+ "call.qualityFair": "مناسب",
+ "call.qualityWeak": "کمزور",
+ "msg.code": "کوڈ",
+ "msg.timer": "ٹائمر",
+ "msg.voice": "آواز",
+ "msg.play": "چلائیں",
+ "msg.sent": "بھیجا گیا",
+ "msg.read": "پڑھا گیا",
+ "report.title": "فوری حفاظتی تصدیق",
+ "report.active": "فعال",
+ "report.testsPassed": "کامیاب تجربات",
+ "report.verifiedAt": "تصدیق کا وقت",
+ "report.source": "ماخذ",
+ "secTest.verifyECDHKeyExchange": "ECDH کلید تبادلہ",
+ "secTest.verifyECDSASignatures": "ECDSA ڈیجیٹل دستخط",
+ "secTest.verifyEncryption": "AES-GCM خفیہ کاری",
+ "secTest.verifyMessageIntegrity": "پیغام کی سالمیت",
+ "secTest.verifyPerfectForwardSecrecy": "مکمل فارورڈ سیکرسی",
+ "secTest.verifyPFS": "مکمل فارورڈ سیکرسی",
+ "secTest.verifyReplayProtection": "بازگشت سے تحفظ",
+ "secTest.verifyDTLSFingerprint": "DTLS نقشِ انگشت",
+ "secTest.verifySASVerification": "SAS تصدیق",
+ "secTest.verifyMetadataProtection": "میٹا ڈیٹا کا تحفظ",
+ "secTest.verifyTrafficObfuscation": "ٹریفک کی دھندلاہٹ",
+ "secTest.verifyPacketPadding": "پیکٹ بھرتی",
+ "secTest.verifyNestedEncryption": "متداخل خفیہ کاری",
+ "secTest.verifyNonExtractableKeys": "ناقابلِ استخراج کلیدیں",
+ "secTest.verifyRateLimiting": "شرح کی حد بندی",
+ "secTest.verifyMutualAuth": "باہمی توثیق",
+ "secTest.verifyAuthProof": "توثیق کا ثبوت",
+ "secTest.verifyEnhancedValidation": "بہتر توثیق",
+ "secTest.verifyAdvancedFeatures": "اعلیٰ خصوصیات",
+ "secTest.verifySignature": "دستخط کی جانچ",
+ "secDetail.Security system initializing...": "حفاظتی نظام شروع ہو رہا ہے...",
+ "secDetail.Nested encryption active": "متداخل خفیہ کاری فعال",
+ "secDetail.Nested encryption failed": "متداخل خفیہ کاری ناکام",
+ "secDetail.Packet padding active": "پیکٹ بھرتی فعال",
+ "secDetail.Packet padding failed": "پیکٹ بھرتی ناکام",
+ "secDetail.Advanced features active": "اعلیٰ خصوصیات فعال",
+ "secDetail.Advanced features failed": "اعلیٰ خصوصیات ناکام",
+ "secDetail.No encryption key available": "کوئی خفیہ کاری کی کلید دستیاب نہیں",
+ "secDetail.AES-GCM encryption/decryption working correctly": "AES-GCM خفیہ کاری اور کشائی درست کام کر رہی ہے",
+ "secDetail.No ECDH key pair available": "کوئی ECDH کلید جوڑا دستیاب نہیں",
+ "secDetail.Key derivation failed": "کلید کا اخذ ناکام",
+ "secDetail.No ECDSA key pair available": "کوئی ECDSA کلید جوڑا دستیاب نہیں",
+ "secDetail.ECDSA digital signatures working correctly": "ECDSA ڈیجیٹل دستخط درست کام کر رہے ہیں",
+ "secDetail.MAC key not available or invalid": "MAC کلید دستیاب نہیں یا غلط ہے",
+ "secDetail.Rate limiter did not block a message over the limit": "شرح محدود کرنے والے نے حد سے زیادہ پیغام نہیں روکا",
+ "secDetail.Rate limiter is not available": "شرح محدود کرنے والا دستیاب نہیں",
+ "secDetail.Replay protection is working correctly": "بازگشت سے تحفظ درست کام کر رہا ہے",
+ "secDetail.Replay protection not enabled": "بازگشت سے تحفظ چالو نہیں",
+ "secDetail.SAS code not available": "SAS کوڈ دستیاب نہیں",
+ "secDetail.SAS verification code is valid and available": "SAS تصدیقی کوڈ درست اور دستیاب ہے",
+ "secDetail.Traffic obfuscation is working correctly": "ٹریفک کی دھندلاہٹ درست کام کر رہی ہے",
+ "secDetail.Traffic obfuscation not enabled": "ٹریفک کی دھندلاہٹ چالو نہیں",
+ "secDetail.No non-extractable ephemeral ECDH key pair for this session": "اس سیشن کے لیے کوئی ناقابلِ استخراج عارضی ECDH کلید جوڑا نہیں",
+ "secDetail.Session-level PFS only: keys are ephemeral per session, but the Double Ratchet is not active for this connection (peer on an older version), so a compromised session key exposes the whole conversation": "صرف سیشن کی سطح پر فارورڈ سیکرسی: کلیدیں ہر سیشن کے لیے عارضی ہیں، مگر اس اتصال میں Double Ratchet فعال نہیں (ساتھی پرانے نسخے پر ہے)، اس لیے ایک افشا شدہ سیشن کلید پوری گفتگو کھول دیتی ہے",
+ "msg.pause": "توقف",
+ "msg.failed": "ناکام",
+ "groupErr.timeout": "کوڈ تیار ہونے سے پہلے ایک رکن نے جواب دینا بند کر دیا۔",
+ "groupErr.keyMismatch": "ایک رکن کی کلید اس کے دعوے کردہ شناخت سے نہیں ملی۔",
+ "groupErr.revealMismatch": "ایک رکن کی افشا کردہ قدر اس کے عہد سے نہیں ملی۔",
+ "groupErr.commitChanged": "ایک رکن نے درمیان میں اپنا عہد بدل دیا۔",
+ "groupErr.missingKey": "ایک ایسا رکن فہرست میں تھا جس کی کلید کبھی نہیں پہنچی۔",
+ "groupErr.outsider": "گروہ سے باہر کسی کی طرف سے فریم آیا۔",
+ "groupErr.limit": "گروہ آٹھ ارکان تک محدود ہے۔",
+ "groupErr.tooLarge": "پیغام گروہ کو بھیجنے کے لیے بہت بڑا تھا۔",
+ "fileType.text": "سادہ متن",
+ "fileType.images": "تصاویر",
+ "fileType.archives": "محفوظہ جات",
+ "fileType.voice": "صوتی پیغامات",
+ "fileType.unsupported": "غیر معاون",
+ "qrScan.title": "QR کوڈ پڑھیں",
+ "qrScan.auto": "خودکار موڈ",
+ "qrScan.reset": "دوبارہ ترتیب",
+ "qrScan.autoScroll": "خودکار سکرول چالو ہے",
+ "qrScan.starting": "کیمرہ شروع ہو رہا ہے...",
+ "qrScan.point": "کیمرہ QR کوڈ کی طرف کریں",
+ "qrScan.tapFocus": "توجہ مرکوز کرنے کے لیے سکرین دبائیں",
+ "qrScan.focusing": "توجہ مرکوز کی جا رہی ہے...",
+ "iceErr.notArray": "سرور کی فہرست ایک صف ہونی چاہیے",
+ "iceErr.invalidJson": "غلط JSON",
+ "iceErr.tooMany": "بہت زیادہ سرور (زیادہ سے زیادہ {max})",
+ "iceErr.invalidEntry": "{label}: غلط اندراج",
+ "iceErr.urlCount": "{label}: 1 سے {max} تک پتے درکار ہیں",
+ "iceErr.turnCreds": "{label}: TURN سرورز کو عموماً صارف نام اور سند درکار ہوتی ہے",
+ "iceUrl.notString": "پتہ ایک سلسلہ ہونا چاہیے",
+ "iceUrl.empty": "پتہ خالی ہے",
+ "iceUrl.tooLong": "پتہ بہت لمبا ہے",
+ "iceUrl.badChars": "پتے میں غلط حروف ہیں",
+ "iceUrl.badScheme": "پتہ stun:، stuns:، turn: یا turns: سے شروع ہونا چاہیے",
+ "iceUrl.badQuery": "پتے کی استفسار غلط ہے",
+ "iceUrl.noHost": "پتے میں میزبان نہیں",
+ "iceUrl.badHost": "پتے کا میزبان یا پورٹ غلط ہے",
+ "iceUrl.badTransport": "پتے کی استفسار transport=udp یا transport=tcp ہونی چاہیے",
+ "msg.off": "بند",
+ "file.consentUnavailable": "صارف کی رضامندی دستیاب نہیں",
+ "secLevel.MAXIMUM": "زیادہ سے زیادہ",
+ "secLevel.INITIALIZING": "شروع ہو رہا ہے",
+ "secLevel.ERROR": "خرابی",
+ "secLevel.UNKNOWN": "نامعلوم",
+ "status.error": "خرابی",
+ "offline.disconnect": "منقطع کریں",
+ "offline.learnMore": "مزید جانیں",
+ "pwa.installApp": "ایپ نصب کریں",
+ "chat.onWeb": "آپ ویب پر ہیں"
}
};
diff --git a/src/i18n/index.js b/src/i18n/index.js
index 792139f..0415e62 100644
--- a/src/i18n/index.js
+++ b/src/i18n/index.js
@@ -110,6 +110,44 @@ export function currentLocale() {
return resolvedLocale;
}
+/**
+ * Writing direction of a locale. Arabic, Hebrew, Persian and Urdu read right to left,
+ * and the whole layout — not just the text — has to follow: an avatar that sits before
+ * a name in English sits after it in Arabic.
+ *
+ * The generated page already carries dir on , so nothing here needs to apply it
+ * at load. This exists for the handful of decisions CSS cannot express on its own —
+ * which way an arrow points, which edge a drawer slides in from.
+ */
+export function localeDir(code = currentLocale()) {
+ return LOCALE_META[code]?.dir === 'rtl' ? 'rtl' : 'ltr';
+}
+
+export function isRTL(code = currentLocale()) {
+ return localeDir(code) === 'rtl';
+}
+
+/**
+ * +1 or -1, for arithmetic on a horizontal offset: a swipe threshold, a translate, the
+ * side a sheet enters from. `x * direction()` is the whole of what a mirrored gesture
+ * needs, and it reads better than an `isRTL ? -x : x` at every call site.
+ */
+export function direction(code = currentLocale()) {
+ return isRTL(code) ? -1 : 1;
+}
+
+/**
+ * Force a fragment to be laid out left to right inside right-to-left text.
+ *
+ * Bidi reordering is done by the browser on the rendered text, and it mangles exactly
+ * the strings this app is made of: a key fingerprint, a base64 session descriptor, a
+ * URL, a version number. In an RTL paragraph "a1b2:c3d4" can come out as "c3d4:a1b2" —
+ * the characters are all there, so nothing looks broken, and the reader compares the
+ * wrong thing against their peer's screen. Any element showing machine text gets these
+ * props.
+ */
+export const LTR_TEXT = { dir: 'ltr', style: { unicodeBidi: 'isolate', textAlign: 'start' } };
+
/**
* A locale the visitor would probably rather read, when it is not the one they are on.
* Used to offer a link, never to redirect: an automatic redirect sends Googlebot —
@@ -150,6 +188,9 @@ export function languageLinks({ pathname = '/', active = DEFAULT_LOCALE } = {})
label: LOCALE_META[code]?.nativeName || code,
// Short code for the collapsed switcher.
abbr: LOCALE_META[code]?.abbr || code.toUpperCase(),
+ // The row renders a name in its own script, so it needs its own direction:
+ // "العربية" laid out left-to-right is the word spelled backwards.
+ dir: LOCALE_META[code]?.dir || 'ltr',
isCurrent: code === active,
}));
}
diff --git a/src/pwa/install-prompt.js b/src/pwa/install-prompt.js
index 5310d03..1b5cab8 100644
--- a/src/pwa/install-prompt.js
+++ b/src/pwa/install-prompt.js
@@ -161,14 +161,14 @@ class PWAInstallPrompt {
this.installButton = document.createElement('div');
this.installButton.id = 'pwa-install-button';
this.installButton.className = 'hidden';
- this.installButton.style.cssText = "position:fixed; bottom:24px; right:24px; z-index:50; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
+ this.installButton.style.cssText = "position:fixed; bottom:24px; inset-inline-end:24px; z-index:50; font-family:'Manrope',system-ui,-apple-system,sans-serif;";
this.installButton.innerHTML = `
-
+
-
+
${t('pwa.installApp')}
@@ -219,7 +219,7 @@ class PWAInstallPrompt {
-
+
${t('pwa.install')}
@@ -360,13 +360,13 @@ class PWAInstallPrompt {
${t('pwa.iosTitle')}
-
+
1
${t('pwa.iosStep1')}
-
+
${t('pwa.iosStep1Hint')}
@@ -391,7 +391,7 @@ class PWAInstallPrompt {
-
+
After installation, open SecureBit from your home screen for the best experience.
@@ -451,7 +451,7 @@ class PWAInstallPrompt {
modal.innerHTML = `
-
+
@@ -499,7 +499,7 @@ class PWAInstallPrompt {
showInstallSuccess() {
const notification = document.createElement('div');
- notification.className = 'fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
+ notification.className = 'fixed top-4 end-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
const successText = this.isIOSSafari() ?
t('pwa.installedIos') :
diff --git a/src/pwa/pwa-manager.js b/src/pwa/pwa-manager.js
index 3a08862..e42100d 100644
--- a/src/pwa/pwa-manager.js
+++ b/src/pwa/pwa-manager.js
@@ -258,7 +258,7 @@ class PWAOfflineManager {
``;
@@ -366,7 +366,7 @@ class PWAOfflineManager {
${t('offline.learnMore')}
-
+
`;
@@ -375,7 +375,7 @@ class PWAOfflineManager {
-
+
${t('offline.restoredTitle')}
@@ -713,7 +713,7 @@ class PWAOfflineManager {
showSyncNotification(successCount, errorCount) {
const notification = document.createElement('div');
- notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
+ notification.className = 'fixed bottom-4 end-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
let message = '';
if (successCount > 0 && errorCount === 0) {
@@ -796,7 +796,7 @@ class PWAOfflineManager {
showReconnectionSuccess() {
const notification = document.createElement('div');
- notification.className = 'fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
+ notification.className = 'fixed top-4 end-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `
@@ -814,7 +814,7 @@ class PWAOfflineManager {
showReconnectionError(error) {
const notification = document.createElement('div');
- notification.className = 'fixed top-4 right-4 bg-yellow-500 text-black p-4 rounded-lg shadow-lg z-50 max-w-sm';
+ notification.className = 'fixed top-4 end-4 bg-yellow-500 text-black p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `
@@ -1125,7 +1125,7 @@ class PWAOfflineManager {
// Show fallback UI
const fallback = document.createElement('div');
- fallback.className = 'fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
+ fallback.className = 'fixed bottom-4 end-4 bg-red-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
fallback.innerHTML = `
@@ -1150,7 +1150,7 @@ class PWAOfflineManager {
helpModal.innerHTML = `
-
+
Offline Mode Guide
@@ -1159,10 +1159,10 @@ class PWAOfflineManager {
-
+
What works offline:
-
+
• App interface and navigation
• Previously cached resources
• Session data and keys (preserved in memory)
@@ -1173,10 +1173,10 @@ class PWAOfflineManager {
-
+
What requires internet:
-
+
• P2P connections (WebRTC)
• Real-time messaging
• Session verification
@@ -1186,10 +1186,10 @@ class PWAOfflineManager {
-
+
Automatic sync:
-
+
When you're back online, all queued messages and actions
will be automatically synchronized. No data is lost.
@@ -1197,7 +1197,7 @@ class PWAOfflineManager {
-
+
Security Notice
diff --git a/src/scripts/pwa-globals.js b/src/scripts/pwa-globals.js
index dd856cd..ab4a411 100644
--- a/src/scripts/pwa-globals.js
+++ b/src/scripts/pwa-globals.js
@@ -70,7 +70,7 @@ window.showUpdateNotification = function showUpdateNotification() {
Update available
A newer version of SecureBit has been detected.
-
+
Current version
${currentStr}
diff --git a/src/scripts/pwa-offline-test.js b/src/scripts/pwa-offline-test.js
index 96ada6a..e448c65 100644
--- a/src/scripts/pwa-offline-test.js
+++ b/src/scripts/pwa-offline-test.js
@@ -145,7 +145,7 @@ class PWAOfflineTester {
modal.innerHTML = `
-
+
PWA Offline Test Results
diff --git a/src/scripts/pwa-register.js b/src/scripts/pwa-register.js
index 7a77a3b..b64e516 100644
--- a/src/scripts/pwa-register.js
+++ b/src/scripts/pwa-register.js
@@ -2,8 +2,14 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
- const registration = await navigator.serviceWorker.register('./sw.js', {
- scope: './',
+ // Root-absolute, both of them. Every locale is its own page in its own
+ // subdirectory, so './sw.js' asks /ar/ for /ar/sw.js — a 404, and no Service
+ // Worker at all on twelve of the thirteen pages. There is one worker for the
+ // whole site and it needs the whole site as its scope, or an installed app
+ // that started at /ar/ would stop being offline-capable the moment it
+ // navigated to the root.
+ const registration = await navigator.serviceWorker.register('/sw.js', {
+ scope: '/',
});
// Store registration for use in other modules
diff --git a/src/styles/components.css b/src/styles/components.css
index 551d82b..b8016da 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -31,7 +31,7 @@
/* Кнопка прокрутки на мобильном */
.scroll-to-bottom-mobile {
bottom: 140px !important;
- right: 16px !important;
+ inset-inline-end: 16px !important;
width: 40px !important;
height: 40px !important;
}
@@ -102,7 +102,7 @@
.progress-text {
position: absolute;
top: -20px;
- right: 0;
+ inset-inline-end: 0;
color: #9ca3af;
}
@@ -218,7 +218,7 @@ main {
height: 8px;
border-radius: 50%;
display: inline-block;
- margin-right: 8px;
+ margin-inline-end: 8px;
}
.status-connected { background: #10b981; }
.status-connecting { background: #6b7280; }
@@ -346,7 +346,7 @@ main {
/* Icon alignment in buttons */
button i {
vertical-align: middle;
- margin-right: 0.5rem;
+ margin-inline-end: 0.5rem;
}
/* Lightning button */
@@ -736,7 +736,14 @@ button i {
/* ============================================================
SecureBit Chat — redesigned chat surface (v4.8.21)
============================================================ */
-.sb-scroll { scrollbar-gutter: auto; }
+/* Firefox has no ::-webkit-scrollbar, so without these two it ignored everything below
+ and drew its default slab — the chat and the language menu looked like different
+ applications depending on the browser. */
+.sb-scroll {
+ scrollbar-gutter: auto;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255,255,255,0.13) transparent;
+}
.sb-scroll::-webkit-scrollbar { width: 9px; }
.sb-scroll::-webkit-scrollbar-track { background: transparent; }
.sb-scroll::-webkit-scrollbar-thumb {
@@ -790,7 +797,7 @@ button i {
.sb-scan-btn:hover { border-color: rgba(62,207,142,0.5) !important; background: rgba(62,207,142,0.1) !important; }
.sb-gen-btn:hover { background: #ff9637 !important; }
.sb-start-card { transition: transform .26s cubic-bezier(.3,.8,.3,1); }
-@media (max-width: 900px) { .sb-start-left { border-right: none !important; } }
+@media (max-width: 900px) { .sb-start-left { border-inline-end: none !important; } }
@media (max-width: 560px) { .sb-start-left { padding: 30px 22px !important; } }
/* PWA install pill belongs to the landing page only — hide it inside the chat. */
body.sb-in-chat #pwa-install-button { display: none !important; }
@@ -802,5 +809,5 @@ body.sb-in-chat #pwa-connection-status {
top: calc(64px + 12px + env(safe-area-inset-top, 0px)) !important;
}
/* The new design spaces icons with flex gap, not icon margins — neutralise the
- global `button i { margin-right: .5rem }` so icons stay centered in their tiles. */
-.sb-start button i, .sb-ice-overlay button i { margin-right: 0; vertical-align: baseline; }
+ global `button i { margin-inline-end: .5rem }` so icons stay centered in their tiles. */
+.sb-start button i, .sb-ice-overlay button i { margin-inline-end: 0; vertical-align: baseline; }
diff --git a/src/styles/main.css b/src/styles/main.css
index 6cbca55..9d88752 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -111,8 +111,7 @@ tr.bg-black-table {
}
.header-minimal .max-w-7xl {
- padding-left: 8px;
- padding-right: 8px;
+ padding-inline: 8px;
}
.header-minimal button {
@@ -123,8 +122,7 @@ tr.bg-black-table {
@media (min-width: 641px) and (max-width: 1024px) {
.header-minimal .max-w-7xl {
- padding-left: 16px;
- padding-right: 16px;
+ padding-inline: 16px;
}
}
@@ -410,7 +408,7 @@ body {
writing-mode: horizontal-tb;
transform: none;
font-size: 1.2rem;
- margin-right: auto;
+ margin-inline-end: auto;
}
.project-card__thumb,
diff --git a/src/styles/pwa.css b/src/styles/pwa.css
index dc025ad..05489ad 100644
--- a/src/styles/pwa.css
+++ b/src/styles/pwa.css
@@ -232,7 +232,7 @@
display: flex;
align-items: center;
justify-content: center;
- margin-right: 16px;
+ margin-inline-end: 16px;
}
.pwa-install-banner .text {
diff --git a/src/styles/rtl.css b/src/styles/rtl.css
new file mode 100644
index 0000000..e978329
--- /dev/null
+++ b/src/styles/rtl.css
@@ -0,0 +1,100 @@
+/* ============================================================================
+ Right-to-left support.
+ ============================================================================
+
+ Almost nothing is here. The layout mirrors because the app was moved onto CSS
+ logical properties — margin-inline-start instead of margin-left, inset-inline-end
+ instead of right — and those follow dir on , which build-i18n.js writes from
+ the locale file. That is the load-bearing change; this sheet is only for the
+ handful of things logical properties cannot express:
+
+ · glyphs that carry a direction, which have to be flipped rather than moved,
+ · text that must NOT mirror — keys, descriptors, URLs, version numbers,
+ · Latin fonts that have no Arabic or Hebrew in them.
+
+ Loaded last so it wins over the utility classes above it. Every rule is scoped to
+ [dir="rtl"], so a left-to-right page is byte-for-byte unaffected.
+ ========================================================================= */
+
+/* ── Typography ──────────────────────────────────────────────────────────────
+ Inter is subsetted to Latin only (deliberately — it is self-hosted to keep
+ fonts.gstatic.com out of the request log, and the CSP has no room for a third
+ party anyway). Its unicode-range simply does not match Arabic or Hebrew, so the
+ browser falls through to the next family in the stack; naming that family here
+ is the difference between SF Arabic and whatever the fallback happens to be.
+
+ Nothing is downloaded for this. An Arabic webfont worth reading is 100 KB+ per
+ weight, and every platform that reads these scripts already ships a good one. */
+[lang="ar"], [lang="fa"], [lang="ur"] {
+ font-family: 'Inter', 'SF Arabic', 'Geeza Pro', 'Segoe UI', Tahoma,
+ 'Noto Naskh Arabic', 'Noto Sans Arabic', 'Droid Arabic Naskh', sans-serif;
+}
+
+/* Urdu is written in Nastaliq, which cascades diagonally and needs far more line
+ height than a Naskh face. Where no Nastaliq font is installed the text falls back
+ to Naskh, which Urdu readers read without trouble — the extra leading is harmless
+ there and essential when Nastaliq does resolve. */
+[lang="ur"] {
+ font-family: 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Urdu Typesetting',
+ 'Inter', 'SF Arabic', 'Segoe UI', Tahoma, 'Noto Naskh Arabic', sans-serif;
+ line-height: 2;
+}
+
+[lang="he"] {
+ font-family: 'Inter', 'SF Hebrew', 'Arial Hebrew', 'Segoe UI', 'Noto Sans Hebrew', sans-serif;
+}
+
+/* ── Text that must not mirror ───────────────────────────────────────────────
+ Bidi reordering is applied to rendered text, and it does not know that a session
+ descriptor is one atom. Inside an RTL paragraph the browser will happily print
+ "a1b2:c3d4" as "c3d4:a1b2": every character is present, nothing looks broken, and
+ the person comparing it against their peer's screen is comparing the wrong thing.
+
+ `direction: ltr` fixes the order; `unicode-bidi: isolate` stops the run from
+ dragging the surrounding sentence around with it. Both are needed. */
+[dir="rtl"] code,
+[dir="rtl"] pre,
+[dir="rtl"] .sb-sc {
+ direction: ltr;
+ unicode-bidi: isolate;
+ text-align: start;
+}
+
+/* Digit shape is left to the font rather than forced here. Everything that has to be
+ read back character by character — the safety code, a key, a byte count — is already
+ set in the monospace stack, which carries no Arabic-Indic substitution, so those
+ digits stay Western without a rule. Prose is free to render its own numerals. */
+
+/* ── Glyphs that carry a direction ───────────────────────────────────────────
+ A chevron pointing at the next item points the other way when "next" is the other
+ way. These are flipped, not repositioned — the element is already in the right
+ place, it is the drawing inside it that is backwards.
+
+ Only genuinely directional icons are listed. fa-share (the iOS share sheet: a box
+ with an arrow leaving the top) and fa-download are vertical and stay put; mirroring
+ them would make them wrong rather than localized. */
+[dir="rtl"] .fa-chevron-left,
+[dir="rtl"] .fa-chevron-right,
+[dir="rtl"] .fa-angle-left,
+[dir="rtl"] .fa-angle-right,
+[dir="rtl"] .fa-arrow-left,
+[dir="rtl"] .fa-arrow-right,
+[dir="rtl"] .fa-caret-left,
+[dir="rtl"] .fa-caret-right,
+[dir="rtl"] .fa-reply,
+[dir="rtl"] .sb-mirror-rtl {
+ transform: scaleX(-1);
+}
+
+/* ── Utility classes assembled at runtime ────────────────────────────────────
+ Tailwind's translate utilities set a custom property that the transform reads, so
+ redefining the property is enough to send a toast in from the other edge. This has
+ to be done in CSS rather than by swapping classes: the toasts add and remove
+ `translate-x-full` by name to animate, and a second class would survive the
+ removal and pin them off-screen. */
+[dir="rtl"] .translate-x-full { --tw-translate-x: -100%; }
+[dir="rtl"] .-translate-x-full { --tw-translate-x: 100%; }
+
+/* The mesh animation on the landing page moves its packet along an offset-path taken
+ from the SVG it travels over, so it mirrors with the drawing and needs nothing here.
+ Scrollbars, gradients and shadows are symmetric in this design; likewise nothing. */
diff --git a/sw.js b/sw.js
index 010a017..dc82571 100644
--- a/sw.js
+++ b/sw.js
@@ -11,12 +11,12 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
-const SW_BUILD_VERSION = '1788021796445';
+const SW_BUILD_VERSION = '1788040116230';
// Locale subdirectories, rewritten by scripts/build-i18n.js. Each localized page is a
// separate document at its own URL, so the shell has to be cached and served per
// locale — otherwise an offline visitor on /de/ is handed the English page back.
-const SW_LOCALES = ['de', 'fr', 'es', 'uk', 'ru', 'zh', 'ko', 'hi'];
+const SW_LOCALES = ['de', 'fr', 'es', 'uk', 'ru', 'zh', 'ko', 'hi', 'ar', 'he', 'fa', 'ur'];
const LOCALE_SHELLS = SW_LOCALES.flatMap((code) => [`/${code}/`, `/${code}/index.html`, `/${code}/manifest.json`]);
// The locale a request belongs to, as a shell path. Falls back to the root shell.
diff --git a/tailwind.config.json b/tailwind.config.json
index 24f3fe3..3ed1939 100644
--- a/tailwind.config.json
+++ b/tailwind.config.json
@@ -1 +1 @@
-{"content":["./index.html","./dist/app.js","./src/**/*.js","./src/**/*.jsx"],"safelist":[{"pattern":"^(bg|text|border|ring|from|to|via)-(red|green|blue|yellow|orange|purple|emerald|cyan|gray|white|black)(-|$)"},{"pattern":"^(bg|text|border|ring)-(gray|slate|zinc|neutral|stone)-(50|100|200|300|400|500|600|700|800|900)$"},{"pattern":"^(bg|text|border|ring)-(orange|blue|green|yellow|red|purple|emerald|cyan)-(100|200|300|400|500|600|700)$"},{"pattern":"^(bg|text|border|ring)-.*\/(10|20|30|40|50|60|70|80|90)$"},{"pattern":"^(w|h|min-w|min-h|max-w|max-h)-\[.*\]$"},{"pattern":"^(top|right|bottom|left)-\[.*\]$"},{"pattern":"^(translate|skew|scale|rotate)-.*$"},{"pattern":"^(opacity|shadow|rounded|flex|grid|gap|p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml)-.*$"}],"theme":{"extend":{}},"plugins":[]}
+{"content":["./index.html","./dist/app.js","./src/**/*.js","./src/**/*.jsx"],"safelist":[{"pattern":"^(bg|text|border|ring|from|to|via)-(red|green|blue|yellow|orange|purple|emerald|cyan|gray|white|black)(-|$)"},{"pattern":"^(bg|text|border|ring)-(gray|slate|zinc|neutral|stone)-(50|100|200|300|400|500|600|700|800|900)$"},{"pattern":"^(bg|text|border|ring)-(orange|blue|green|yellow|red|purple|emerald|cyan)-(100|200|300|400|500|600|700)$"},{"pattern":"^(bg|text|border|ring)-.*\/(10|20|30|40|50|60|70|80|90)$"},{"pattern":"^(w|h|min-w|min-h|max-w|max-h)-\[.*\]$"},{"pattern":"^(top|right|bottom|left)-\[.*\]$"},{"pattern":"^(start|end)-.*$"},{"pattern":"^(translate|skew|scale|rotate)-.*$"},{"pattern":"^(opacity|shadow|rounded|flex|grid|gap|p|px|py|pt|pr|pb|pl|ps|pe|m|mx|my|mt|mr|mb|ml|ms|me)-.*$"}],"theme":{"extend":{}},"plugins":[]}
diff --git a/templates/index.template.html b/templates/index.template.html
index 2daa293..75103a3 100644
--- a/templates/index.template.html
+++ b/templates/index.template.html
@@ -1,7 +1,7 @@
-
+
+
+
diff --git a/tests/asset-paths-locale-safe.test.mjs b/tests/asset-paths-locale-safe.test.mjs
index ffc9c90..bdb526f 100644
--- a/tests/asset-paths-locale-safe.test.mjs
+++ b/tests/asset-paths-locale-safe.test.mjs
@@ -19,6 +19,10 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
// locale page has to start with a slash.
const ROOT_DIRS = ['logo', 'assets', 'libs', 'dist', 'config', 'src'];
+// Prose about a path is not a path. A comment quoting './sw.js' to explain why it is
+// wrong must not itself be reported as the thing being wrong.
+const isComment = (line) => /^\s*(\/\/|\/?\*)/.test(line);
+
const files = execFileSync('git', ['ls-files', 'src'], { cwd: ROOT, encoding: 'utf8' })
.trim().split('\n')
.filter((f) => /\.(js|jsx)$/.test(f) && f !== 'src/i18n/generated.js');
@@ -27,6 +31,7 @@ const offenders = [];
for (const file of files) {
const text = readFileSync(path.join(ROOT, file), 'utf8');
text.split('\n').forEach((line, i) => {
+ if (isComment(line)) return;
// A quoted path that starts with a root directory name and no leading slash.
for (const match of line.matchAll(new RegExp(`['"](?:${ROOT_DIRS.join('|')})/[A-Za-z0-9_./-]+\\.(png|jpe?g|svg|gif|webp|ico|css|mp3|mp4|webm|woff2?)['"]`, 'g'))) {
// An ES import specifier is resolved by the bundler at build time, not by the
@@ -37,9 +42,38 @@ for (const file of files) {
});
}
+// The check above only knows about root *directories*, so a root-level *file* walked
+// straight past it: the Service Worker was registered as './sw.js', which asks /ar/ for
+// /ar/sw.js and gets a 404. That left twelve of the thirteen pages with no worker at all
+// — no offline shell, no update prompt — and it was invisible from the English page,
+// which is the only one where the relative path happens to be right.
+const ROOT_FILES = ['sw.js', 'manifest.json', 'meta.json', 'robots.txt', 'sitemap.xml', 'browserconfig.xml'];
+for (const file of files) {
+ const text = readFileSync(path.join(ROOT, file), 'utf8');
+ text.split('\n').forEach((line, i) => {
+ if (isComment(line)) return;
+ for (const match of line.matchAll(new RegExp(`['"](?:\\./)?(?:${ROOT_FILES.join('|').replace(/\./g, '\\.')})['"]`, 'g'))) {
+ if (/\b(import|from|require)\b/.test(line)) continue;
+ offenders.push(`${file}:${i + 1} ${match[0]}`);
+ }
+ });
+}
+
+// A worker scoped to './' controls only the directory it was registered from, so an app
+// installed at /ar/ would stop being covered the moment it navigated to the root. There
+// is one worker for the whole site; its scope is the whole site.
+for (const file of files) {
+ const text = readFileSync(path.join(ROOT, file), 'utf8');
+ if (!text.includes('serviceWorker.register')) continue;
+ text.split('\n').forEach((line, i) => {
+ if (isComment(line)) return;
+ if (/scope:\s*['"](?!\/['"])/.test(line)) offenders.push(`${file}:${i + 1} ${line.trim()}`);
+ });
+}
+
assert.deepEqual(
offenders, [],
- 'these asset paths are relative and will 404 from a locale subdirectory — prefix them with "/":\n' +
+ 'these paths are relative and change meaning inside a locale subdirectory — make them root-absolute:\n' +
offenders.join('\n')
);
diff --git a/tests/i18n-build.test.mjs b/tests/i18n-build.test.mjs
index 32e1fe0..ed425fd 100644
--- a/tests/i18n-build.test.mjs
+++ b/tests/i18n-build.test.mjs
@@ -47,7 +47,7 @@ for (const code of site.locales) {
const html = read(pageFor(code));
const locale = localeFile(code);
- assert.match(html, new RegExp(``), `${code}: wrong `);
+ assert.match(html, new RegExp(``), `${code}: wrong /`);
assert.ok(
html.includes(` `),
`${code}: canonical must point at its own URL, not another locale's`
@@ -181,7 +181,10 @@ for (const code of site.locales) {
const en = JSON.parse(read('locales/en.json'));
const xx = JSON.parse(JSON.stringify(en));
- Object.assign(xx, { htmlLang: 'xx', ogLocale: 'xx_XX', nativeName: 'Test' });
+ // Right-to-left on purpose: direction is carried from the locale file all the way
+ // into and the per-locale manifest, and the throwaway site is where that
+ // plumbing can be exercised without a real RTL locale having to be the one under test.
+ Object.assign(xx, { htmlLang: 'xx', ogLocale: 'xx_XX', nativeName: 'Test', dir: 'rtl' });
xx.manifest = { name: 'XX name', short_name: 'XX', description: 'XX description' };
writeFileSync(path.join(localesDir, 'en.json'), JSON.stringify(en, null, 2));
writeFileSync(path.join(localesDir, 'xx.json'), JSON.stringify(xx, null, 2));
@@ -202,7 +205,10 @@ for (const code of site.locales) {
// The secondary locale points at itself, not at the default one.
assert.ok(secondary.includes(' '),
'a secondary locale must be canonical to itself, or Google will not index it');
- assert.match(secondary, //);
+ assert.match(secondary, //,
+ 'the locale\'s writing direction must be on , not applied later by the app');
+ assert.match(primary, //,
+ 'a left-to-right locale must still say so explicitly');
// hreflang has to be reciprocal: both pages list both locales plus x-default.
for (const page of [primary, secondary]) {
@@ -223,6 +229,7 @@ for (const code of site.locales) {
assert.ok(secondary.includes(' '));
const manifest = JSON.parse(at('xx/manifest.json'));
assert.equal(manifest.lang, 'xx');
+ assert.equal(manifest.dir, 'rtl', 'an installed RTL locale must launch right-to-left');
assert.equal(manifest.start_url, '/xx/', 'an installed locale must launch into its own page');
assert.equal(manifest.scope, '/', 'scope must still cover the whole site');
assert.equal(manifest.name, 'XX name', 'manifest name should come from the locale file');
diff --git a/tests/i18n-runtime.test.mjs b/tests/i18n-runtime.test.mjs
index 500efce..a1bff91 100644
--- a/tests/i18n-runtime.test.mjs
+++ b/tests/i18n-runtime.test.mjs
@@ -23,6 +23,43 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
);
}
+// The dictionaries have to reach the bundle the browser actually downloads, not just
+// the module on disk. `build:js` bundles whatever src/i18n/generated.js says at the
+// moment it runs, so if generation happened after bundling, a newly added language
+// would ship a correct page — right , right — wrapped around an
+// app that has never heard of it and quietly falls back to English. That failure looks
+// like a translation bug and is really a build-order bug, so it is pinned here.
+{
+ const bundle = readFileSync(path.join(ROOT, 'dist/app.js'), 'utf8');
+ const { SUPPORTED_LOCALES, DICTIONARIES } = await import(
+ pathToFileURL(path.join(ROOT, 'src/i18n/generated.js'))
+ );
+
+ // esbuild escapes anything outside ASCII, so nothing but pure Latin is findable as
+ // literal text: Latin-1 becomes \xNN, everything above it \uXXXX, both uppercase.
+ // Escape the needle the same way before looking for it.
+ const asEmitted = (text) =>
+ [...text]
+ .map((ch) => {
+ const code = ch.codePointAt(0);
+ if (code < 0x80) return ch;
+ const hex = (n, width) => n.toString(16).toUpperCase().padStart(width, '0');
+ if (code <= 0xff) return `\\x${hex(code, 2)}`;
+ return [...ch].map((unit) => `\\u${hex(unit.charCodeAt(0), 4)}`).join('');
+ })
+ .join('');
+
+ const absent = SUPPORTED_LOCALES.filter(
+ (code) => !bundle.includes(asEmitted(DICTIONARIES[code]['hero.headlineTop']))
+ );
+ assert.deepEqual(absent, [],
+ `dist/app.js carries no strings for: ${absent.join(', ')} — run \`npm run build\`, ` +
+ 'and check that build:i18n still runs before build:js');
+
+ assert.ok(bundle.includes(`SUPPORTED_LOCALES = ${JSON.stringify(SUPPORTED_LOCALES).replace(/,/g, ', ')}`),
+ 'the bundled locale registry disagrees with src/i18n/generated.js');
+}
+
// The real module, as shipped.
{
const live = await import(pathToFileURL(path.join(ROOT, 'src/i18n/index.js')));
diff --git a/tests/rtl-layout.test.mjs b/tests/rtl-layout.test.mjs
new file mode 100644
index 0000000..0fcd64e
--- /dev/null
+++ b/tests/rtl-layout.test.mjs
@@ -0,0 +1,188 @@
+// Right-to-left is a layout property, not a translation. Arabic strings in a left-to-right
+// page are not a localized app — the avatar still sits on the wrong side of the name, the
+// drawer still flies in from the wrong edge, the chevron still points away from "next".
+//
+// So this test guards the mechanism rather than the wording: that direction reaches the
+// document, that the source no longer hard-codes a physical side, and that the two things
+// which must NOT mirror — the swipe maths and machine-readable text — are handled.
+//
+// It reads the source rather than rendering it because there is no DOM here that resolves
+// logical properties: jsdom parses `margin-inline-start` but has no layout engine to
+// mirror it, so an assertion about pixels would be an assertion about jsdom.
+
+import assert from 'node:assert/strict';
+import { readFileSync, readdirSync, statSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+const read = (rel) => readFileSync(path.join(ROOT, rel), 'utf8');
+const site = JSON.parse(read('locales/site.json'));
+
+/** Every source file the UI is built from. */
+function sources() {
+ const out = [];
+ const walk = (dir) => {
+ for (const entry of readdirSync(path.join(ROOT, dir))) {
+ const rel = `${dir}/${entry}`;
+ if (statSync(path.join(ROOT, rel)).isDirectory()) walk(rel);
+ else if (/\.(jsx?|css)$/.test(entry) && !rel.endsWith('i18n/generated.js')) out.push(rel);
+ }
+ };
+ walk('src');
+ return out;
+}
+
+// ── The direction reaches the page ───────────────────────────────────────────────────
+// It has to be in the served HTML, not applied by the app: styling and first paint both
+// happen long before React mounts, so a direction set in JS shows one frame of a mirrored
+// layout to every RTL reader.
+{
+ const rtl = site.locales.filter((code) => JSON.parse(read(`locales/${code}.json`)).dir === 'rtl');
+ assert.ok(rtl.length > 0, 'no RTL locale is registered — this test has nothing to protect');
+
+ for (const code of rtl) {
+ const locale = JSON.parse(read(`locales/${code}.json`));
+ const page = read(code === site.defaultLocale ? 'index.html' : `${code}/index.html`);
+ assert.match(page, new RegExp(``),
+ `${code}: the generated page does not declare its writing direction`);
+ assert.equal(JSON.parse(read(`${code}/manifest.json`)).dir, 'rtl',
+ `${code}: an installed PWA would launch left-to-right`);
+ }
+
+ assert.ok(read('templates/index.template.html').includes('src/styles/rtl.css'),
+ 'the mirroring stylesheet is not linked from the template');
+}
+
+// ── The runtime exposes direction ────────────────────────────────────────────────────
+{
+ const i18n = await import(pathToFileURL(path.join(ROOT, 'src/i18n/index.js')));
+ const rtl = site.locales.find((code) => JSON.parse(read(`locales/${code}.json`)).dir === 'rtl');
+
+ assert.equal(i18n.localeDir(site.defaultLocale), 'ltr');
+ assert.equal(i18n.localeDir(rtl), 'rtl');
+ assert.equal(i18n.isRTL(rtl), true);
+ assert.equal(i18n.isRTL(site.defaultLocale), false);
+ assert.equal(i18n.localeDir('nope'), 'ltr', 'an unknown locale must not throw or mirror');
+
+ // The sign is what call sites multiply an offset by, so it is the part that breaks
+ // silently: a drawer with the wrong sign slides off-screen instead of open.
+ assert.equal(i18n.direction(site.defaultLocale), 1);
+ assert.equal(i18n.direction(rtl), -1);
+
+ assert.equal(i18n.LTR_TEXT.dir, 'ltr');
+ assert.equal(i18n.LTR_TEXT.style.unicodeBidi, 'isolate',
+ 'an LTR run inside RTL text must be isolated, or it drags the sentence around it');
+}
+
+// ── No physical sides left in the UI ─────────────────────────────────────────────────
+// A single margin-left is enough to put an icon on the wrong side of its label, and it is
+// invisible to anyone testing in English. Logical properties are the whole mechanism, so
+// a regression here is a regression in RTL support.
+{
+ // camelCase in React style objects, kebab-case in stylesheets and injected CSS strings.
+ const banned = [
+ [/\bmarginLeft\s*:/, 'marginLeft → marginInlineStart'],
+ [/\bmarginRight\s*:/, 'marginRight → marginInlineEnd'],
+ [/\bpaddingLeft\s*:/, 'paddingLeft → paddingInlineStart'],
+ [/\bpaddingRight\s*:/, 'paddingRight → paddingInlineEnd'],
+ [/\bborderLeft\s*:/, 'borderLeft → borderInlineStart'],
+ [/\bborderRight\s*:/, 'borderRight → borderInlineEnd'],
+ [/textAlign\s*:\s*['"](?:left|right)['"]/, "textAlign: 'left'/'right' → 'start'/'end'"],
+ [/(? f !== 'src/styles/rtl.css');
+
+ // A notch is on the physical left of the handset whichever way the text runs, so
+ // safe-area insets are the one place where a physical side is the correct answer.
+ const physicalOnPurpose = (line) => line.includes('env(safe-area-inset-');
+
+ const offences = [];
+ for (const file of files) {
+ const lines = read(file).split('\n');
+ lines.forEach((line, i) => {
+ if (physicalOnPurpose(line)) return;
+ for (const [pattern, fix] of banned) {
+ if (pattern.test(line)) offences.push(`${file}:${i + 1} ${fix}`);
+ }
+ });
+ }
+ assert.deepEqual(offences, [], `physical directions left in the UI:\n ${offences.join('\n ')}`);
+}
+
+// ── The gesture mirrors with the layout ──────────────────────────────────────────────
+// The drawer keeps one logical offset (0 open, -width closed) in both directions, and
+// mirrors at exactly two points: the pixels it paints and the finger that drives it. Miss
+// either and the panel tracks the wrong way under the thumb.
+{
+ const app = read('src/app.jsx');
+ assert.match(app, /const DIR = direction\(\);/, 'app.jsx does not resolve the layout direction');
+ assert.match(app, /translate3d\(' \+ \(x \* DIR\)/, 'the drawer paints an unmirrored offset');
+ assert.match(app, /d\.base \+ \(e\.clientX - d\.x0\) \* DIR/, 'the drawer drag is not mirrored');
+ assert.ok(app.includes('d.vel.add(e.clientX * DIR'),
+ 'the velocity tracker samples raw screen x, so a flick would settle the wrong way');
+}
+
+// ── Machine text is pinned left-to-right ─────────────────────────────────────────────
+// Bidi reordering rewrites exactly the strings this app is made of. A session descriptor
+// or a SAS code printed in the wrong order still looks like a key, so the reader compares
+// it happily against their peer's screen and confirms a channel they never verified.
+{
+ const app = read('src/app.jsx');
+ for (const field of ['value: sasInput', 'value: answerInput', 'value: offerInput']) {
+ const at = [...app.matchAll(new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'))]
+ .map((m) => m.index);
+ assert.notEqual(at.length, 0, `${field} is gone — this guard now protects nothing`);
+ // Every one of them, not just the first: the app renders the SAS code in two
+ // places, and it was the one further up the file that got missed.
+ for (const i of at) {
+ assert.ok(app.slice(Math.max(0, i - 260), i).includes("dir: 'ltr'"),
+ `the field holding ${field} at offset ${i} must be dir="ltr" or bidi reorders what the user compares`);
+ }
+ }
+
+ // Displaying a code is as dangerous as typing one, and easier to miss. The safety
+ // code is drawn one character per tile in a flex row, and a flex row follows the
+ // writing direction — so in Arabic the first character lands on the right and the
+ // code reads backwards. It still looks like a code, which is the whole problem.
+ const cells = app.indexOf("key: 'cells'");
+ assert.notEqual(cells, -1, 'the safety-code tiles are gone — this guard now protects nothing');
+ assert.ok(app.slice(cells, cells + 60).includes("dir: 'ltr'"),
+ 'the safety-code tiles sit in a flex row, so they must be pinned left-to-right');
+
+ // The group code, wherever it is put on screen. A plain `group.sasCode` also appears
+ // in guard conditions, which render nothing — so the window looks both ways and only
+ // the sites that are actually inside an element have to carry the direction.
+ const group = read('src/components/ui/GroupChat.jsx');
+ // `!group.sasCode` is an existence check that renders nothing; everything else puts
+ // the digits somewhere a person can read them.
+ const shown = [...group.matchAll(/(!?)group\.sasCode/g)]
+ .filter((m) => m[1] !== '!')
+ .map((m) => m.index);
+ assert.ok(shown.length >= 2, 'the group safety code is no longer rendered where expected');
+ let pinned = 0;
+ for (const i of shown) {
+ if (group.slice(Math.max(0, i - 420), i + 260).includes("dir: 'ltr'")) pinned += 1;
+ }
+ assert.equal(pinned, shown.length,
+ `${shown.length - pinned} of ${shown.length} group-safety-code sites are not pinned ` +
+ 'left-to-right — everyone in the group compares those digits against each other');
+
+ const rtlCss = read('src/styles/rtl.css');
+ for (const selector of ['[dir="rtl"] code', '[dir="rtl"] pre', '[dir="rtl"] .sb-sc']) {
+ assert.ok(rtlCss.includes(selector), `rtl.css no longer isolates ${selector}`);
+ }
+ assert.match(rtlCss, /unicode-bidi:\s*isolate/, 'isolation is what stops a run dragging its neighbours');
+}
+
+console.log('rtl-layout.test.mjs: all assertions passed');
diff --git a/uk/index.html b/uk/index.html
index ca15312..ae838f2 100644
--- a/uk/index.html
+++ b/uk/index.html
@@ -1,7 +1,7 @@
-
+
-
+
@@ -96,7 +96,7 @@
-
+
@@ -105,7 +105,7 @@
-
+
@@ -135,6 +135,10 @@
+
+
+
+
@@ -152,6 +156,10 @@
+
+
+
+
@@ -214,7 +222,7 @@
-
+
-
-
+
+
@@ -231,35 +239,38 @@
-
-
-
+
+
+
-
+
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/ur/index.html b/ur/index.html
new file mode 100644
index 0000000..889a31c
--- /dev/null
+++ b/ur/index.html
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SecureBit.chat - نجی اور خفیہ کاری شدہ پیغام رساں
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ur/manifest.json b/ur/manifest.json
new file mode 100644
index 0000000..d5a4900
--- /dev/null
+++ b/ur/manifest.json
@@ -0,0 +1,121 @@
+{
+ "name": "SecureBit.chat - نجی اور خفیہ کاری شدہ پیغام رساں",
+ "short_name": "SecureBit",
+ "description": "ECDH، DTLS اور SAS سے محفوظ ہمسطح پیغام رساں، فوجی درجے کی خفیہ کاری کے ساتھ",
+ "start_url": "/ur/",
+ "display": "standalone",
+ "background_color": "#1a1a1a",
+ "theme_color": "#ff6b35",
+ "orientation": "portrait-primary",
+ "scope": "/",
+ "lang": "ur",
+ "dir": "rtl",
+ "categories": [
+ "communication",
+ "security",
+ "productivity"
+ ],
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "/logo/icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-128x128.png",
+ "sizes": "128x128",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-152x152.png",
+ "sizes": "152x152",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-180x180.png",
+ "sizes": "180x180",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-256x256.png",
+ "sizes": "256x256",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-384x384.png",
+ "sizes": "384x384",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/logo/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/logo/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "Create Channel",
+ "short_name": "Create",
+ "description": "Create a new secure channel",
+ "url": "/ur/?action=create",
+ "icons": [
+ {
+ "src": "/logo/icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ }
+ ]
+ },
+ {
+ "name": "Join Channel",
+ "short_name": "Join",
+ "description": "Join an existing secure channel",
+ "url": "/ur/?action=join",
+ "icons": [
+ {
+ "src": "/logo/icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ }
+ ]
+ }
+ ]
+}
diff --git a/zh/index.html b/zh/index.html
index faa262c..20b0e06 100644
--- a/zh/index.html
+++ b/zh/index.html
@@ -1,7 +1,7 @@
-
+
-
+
@@ -96,7 +96,7 @@
-
+
@@ -105,7 +105,7 @@
-
+
@@ -135,6 +135,10 @@
+
+
+
+
@@ -152,6 +156,10 @@
+
+
+
+
@@ -214,7 +222,7 @@
-
+
-
-
+
+
@@ -231,35 +239,38 @@
-
-
-
+
+
+
-
+
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file