diff --git a/src/components/crew/CordnNativePanel.tsx b/src/components/crew/CordnNativePanel.tsx index d133cbe..5d6a331 100644 --- a/src/components/crew/CordnNativePanel.tsx +++ b/src/components/crew/CordnNativePanel.tsx @@ -1,12 +1,39 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ReactNode } from 'react'; -import { KeyRound, Lock, Plus, RefreshCw, Send, UserPlus, Copy, Check, ImagePlus, Loader2, Share2 } from 'lucide-react'; +import { + AlertTriangle, + Check, + ChevronDown, + Copy, + ImagePlus, + KeyRound, + Loader2, + Lock, + Plus, + QrCode, + RefreshCw, + Send, + Share2, + UserPlus, + Users, + X, +} from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { QRCodeCanvas } from '@/components/ui/qrcode'; import { ImageWithFallback } from '@/components/ImageWithFallback'; import { useTranslation } from '@/contexts/language'; +import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useCordnNative } from '@/hooks/useCordnNative'; @@ -23,6 +50,14 @@ interface CordnNativePanelProps { isMember: boolean; } +type MemberRole = 'founder' | 'admin' | 'member'; + +interface PendingMessage { + text: string; + imageUrl?: string; + state: 'sending' | 'failed'; +} + /** * The crew's **native Cordn (MLS) chat** — real end-to-end encryption between * members, routed through the crew's coordinator. See `lib/cordn/native/`. @@ -31,23 +66,83 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel const { t } = useTranslation(); const { user } = useCurrentUser(); const native = useCordnNative({ crew, enabled: !!user && isMember }); + // Stable callbacks (the hook's return object is recreated each render). + const sendNative = native.send; + const checkInviteNative = native.checkInvite; const [draft, setDraft] = useState(''); const [copied, setCopied] = useState(false); + const [copiedLink, setCopiedLink] = useState(false); const [notice, setNotice] = useState(null); + const [awaiting, setAwaiting] = useState(false); + const [pending, setPending] = useState(null); + const [sentIds, setSentIds] = useState>(() => new Set()); + const [inviteOpen, setInviteOpen] = useState(false); const scrollRef = useRef(null); const fileRef = useRef(null); + const checkingRef = useRef(false); + const loadedRequestsRef = useRef(false); const { mutateAsync: uploadFile, isPending: uploading } = useUploadFile(); + const inviteUrl = useMemo( + () => `${window.location.origin}/#/crew/${crew.pubkey}/${crew.identifier}?tab=mls`, + [crew.pubkey, crew.identifier], + ); + useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }); - }, [native.messages.length]); + }, [native.messages.length, pending?.state]); + + useEffect(() => { + if (native.group && awaiting) setAwaiting(false); + }, [native.group, awaiting]); + + // Founders should see waiting requests without hunting for the check button. + // One quiet read when the group opens; the manual button handles the rest. + const loadRequestsNative = native.loadRequests; + useEffect(() => { + if (!canManage || !native.group || !native.ready || loadedRequestsRef.current) return; + loadedRequestsRef.current = true; + void loadRequestsNative(); + }, [canManage, native.group, native.ready, loadRequestsNative]); + + const markSent = useCallback((id: string) => { + if (!id) return; + setSentIds((prev) => new Set(prev).add(id)); + window.setTimeout(() => { + setSentIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }, 3000); + }, []); + + const deliver = useCallback( + async (text: string, imageUrl?: string) => { + setNotice(null); + setPending({ text, imageUrl, state: 'sending' }); + const sent = await sendNative(text, imageUrl); + if (sent) { + markSent(sent.id); + setPending(null); + } else { + setPending((current) => (current ? { ...current, state: 'failed' } : current)); + } + }, + [sendNative, markSent], + ); const handleSend = useCallback(async () => { const body = draft.trim(); - if (!body) return; + if (!body || native.busy) return; setDraft(''); - await native.send(body); - }, [draft, native]); + await deliver(body); + }, [draft, native.busy, deliver]); + + const retrySend = useCallback(async () => { + if (!pending || pending.state !== 'failed') return; + await deliver(pending.text, pending.imageUrl); + }, [pending, deliver]); const onPickImage = useCallback( async (file: File | undefined) => { @@ -58,12 +153,12 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel if (!url) throw new Error('upload returned no url'); const caption = draft.trim(); setDraft(''); - await native.send(caption, url); + await deliver(caption, url); } catch { - // The hook surfaces nothing on failure; keep the composer usable. + setNotice(t('cordnUi.uploadFailed')); } }, - [uploadFile, draft, native], + [uploadFile, draft, deliver, t], ); const copyRef = useCallback(async () => { @@ -77,35 +172,93 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel } }, [native.groupRef]); + const copyInvite = useCallback(async () => { + try { + await navigator.clipboard.writeText(inviteUrl); + setCopiedLink(true); + window.setTimeout(() => setCopiedLink(false), 1500); + } catch { + // clipboard blocked — the link is visible to select. + } + }, [inviteUrl]); + + const checkNow = useCallback(async () => { + if (checkingRef.current) return false; + checkingRef.current = true; + try { + return await checkInviteNative(); + } finally { + checkingRef.current = false; + } + }, [checkInviteNative]); + const handleRequestJoin = useCallback(async () => { setNotice(null); - await native.requestJoin(); - setNotice(t('cordnUi.requested')); - }, [native, t]); + const requested = await native.requestJoin(); + if (!requested) return; + setAwaiting(true); + }, [native]); const handleCheckInvite = useCallback(async () => { + if (native.busy) return; setNotice(null); - const joined = await native.checkInvite(); - if (!joined) setNotice(t('cordnUi.noInvite')); - }, [native, t]); + const joined = await checkNow(); + if (joined) { + setAwaiting(false); + return; + } + if (!awaiting) setNotice(t('cordnUi.noInvite')); + }, [checkNow, awaiting, t, native.busy]); + + // Poll the coordinator while a join request is pending. Backs off from 10s to + // 30s so a founder who approves later is picked up without a manual tap. + // Interactive signers (Amber) are excluded — polling would pop their signer. + useEffect(() => { + if (!awaiting || native.group || !native.ready || native.interactiveHintVisible) return; + let cancelled = false; + let timer: number | undefined; + let delay = 10_000; + + const tick = async () => { + if (cancelled) return; + const joined = await checkNow(); + if (cancelled) return; + if (joined) { + setAwaiting(false); + setNotice(null); + return; + } + delay = Math.min(30_000, Math.round(delay * 1.5)); + timer = window.setTimeout(() => void tick(), delay); + }; + + timer = window.setTimeout(() => void tick(), delay); + return () => { + cancelled = true; + if (timer !== undefined) window.clearTimeout(timer); + }; + }, [awaiting, native.group, native.ready, native.interactiveHintVisible, checkNow]); // "Join our crew chat" invite. The chat needs the person to be a crew member // first, so the link just drops them on this crew's private-chat tab with the // "Ask to join" flow ready. Uses the OS share sheet where available. const shareInvite = useCallback(async () => { - const url = `${window.location.origin}/#/crew/${crew.pubkey}/${crew.identifier}?tab=mls`; try { if (navigator.share) { - await navigator.share({ title: crew.name, text: t('cordnUi.shareInviteText', { crew: crew.name }), url }); + await navigator.share({ + title: crew.name, + text: t('cordnUi.shareInviteText', { crew: crew.name }), + url: inviteUrl, + }); return; } - await navigator.clipboard.writeText(url); + await navigator.clipboard.writeText(inviteUrl); setNotice(t('cordnUi.inviteCopied')); window.setTimeout(() => setNotice(null), 2500); } catch { // share cancelled or clipboard blocked — nothing to do } - }, [crew, t]); + }, [crew.name, inviteUrl, t]); if (!user) { return } text={t('cordnUi.notLoggedIn')} />; @@ -118,29 +271,86 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel } const status = native.ready ? t('cordnUi.ready') : t('cordnUi.connecting'); + const steps = canManage + ? [ + t('cordnUi.onboardingFounder1'), + t('cordnUi.onboardingFounder2'), + t('cordnUi.onboardingFounder3'), + ] + : [ + t('cordnUi.onboardingMember1'), + t('cordnUi.onboardingMember2'), + t('cordnUi.onboardingMember3'), + ]; + + const roleOf = (pubkey: string): MemberRole => + pubkey === crew.pubkey ? 'founder' : crew.admins.includes(pubkey) ? 'admin' : 'member'; return ( -
+
{t('cordnUi.title')} -
+
{native.group && ( {t('cordnUi.memberCount', { count: native.group.members.length })} )} - - {status} - + {native.error ? ( + <> + + {t('cordnUi.unreachable')} + + + + ) : ( + <> + + {status} + + {!native.ready && ( + + )} + + )} + {native.group && ( + + )}

{t('cordnUi.subtitle')}

@@ -148,16 +358,54 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel {native.error && ( -
- {t('cordnUi.errorPrefix')}: {native.error} +
+
+

{t('cordnUi.errorUnreachable')}

+ +
+
+ + {t('cordnUi.errorDetails')} + +

+ {native.error} +

+
)} {!native.group ? ( -
+

- {canManage ? t('cordnUi.noGroupAdmin') : t('cordnUi.noGroupMember')} + {canManage + ? t('cordnUi.noGroupAdmin') + : native.keyPackageReady + ? t('cordnUi.noGroupMember') + : t('cordnUi.setupDeviceBody')}

+ +
    + {steps.map((step, index) => ( +
  1. + + {index + 1} + + {step} +
  2. + ))} +
+ +

+ {t('cordnUi.encryptedNote')} +

+
{canManage ? ( ) : ( <>
- {notice && ( -

{notice}

+ + {awaiting && ( +

+ {native.interactiveHintVisible ? ( + <> + {t('cordnUi.waitingApprovalManual')} + + ) : ( + <> + {t('cordnUi.waitingApproval')} + + )} +

)} + + {notice &&

{notice}

} + + void copyRef()} + />
) : ( <> +
+ + + {t('cordnUi.members')} + · {native.group.members.length} + + +
+ {native.group.members.map((member) => ( + + ))} +
+
+
- {native.messages.length === 0 ? ( -

{t('cordnUi.placeholder')}

+ {native.messages.length === 0 && !pending ? ( +

+ {t('cordnUi.placeholder')} +

) : ( native.messages.map((m) => (
- {m.sender === user.pubkey ? t('cordnUi.you') : shortNpub(m.sender)} - {formatDistanceToNow(new Date(m.createdAt * 1000), { addSuffix: true })} + + {m.sender === user.pubkey ? t('cordnUi.you') : shortNpub(m.sender)} + + + {formatDistanceToNow(new Date(m.createdAt * 1000), { addSuffix: true })} + + {m.direction === 'outbound' && sentIds.has(m.id) && ( + + {t('cordnUi.sent')} + + )}
)} - {m.content &&
{m.content}
} + {m.content && ( +
{m.content}
+ )}
)) )} + + {pending && ( +
+
+ {t('cordnUi.you')} + + {pending.state === 'sending' + ? t('cordnUi.sending') + : t('cordnUi.sendFailed')} + +
+
+ {pending.imageUrl && ( + + )} + {pending.text && ( +
{pending.text}
+ )} +
+ {pending.state === 'failed' && ( +
+ + +
+ )} +
+ )}
@@ -253,7 +605,11 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel onClick={() => fileRef.current?.click()} aria-label={t('cordnUi.attachImage')} > - {uploading ? : } + {uploading ? ( + + ) : ( + + )} void handleSend()} aria-label={t('cordnUi.send')} > - + {native.busy ? ( + + ) : ( + + )}
- {native.groupRef && ( -
- {t('cordnUi.groupRef')} - - {native.groupRef} - - -
- )} + {notice &&

{notice}

} {native.interactiveHintVisible && (

{t('cordnUi.interactiveHint')}

)} + void copyRef()} + /> + {canManage && (
- {t('cordnUi.requests')} -
- - -
+ + {t('cordnUi.requests')} + +
{native.requests.length === 0 ? (

- {native.group.members.length <= 1 ? t('cordnUi.inviteHint') : t('cordnUi.noRequests')} + {native.group.members.length <= 1 + ? t('cordnUi.inviteHint') + : t('cordnUi.noRequests')}

) : ( native.requests.map((req) => ( -
- {shortNpub(req.pk)} - -
+ void native.approve(req.pk)} + /> )) )}
@@ -355,10 +696,186 @@ export function CordnNativePanel({ crew, canManage, isMember }: CordnNativePanel )} + + + + + + {t('cordnUi.qrTitle')} + + {t('cordnUi.qrHint')} + +
+ +
+ + {inviteUrl} + +
+ + +
+
+
); } +function AdvancedPanel({ + groupRef, + coordinatorPubkey, + keyPackageReady, + copied, + onCopyRef, +}: { + groupRef: string | null; + coordinatorPubkey: string; + keyPackageReady: boolean; + copied: boolean; + onCopyRef: () => void; +}) { + const { t } = useTranslation(); + return ( +
+ + + {t('cordnUi.advanced')} + +
+
+ {t('cordnUi.coordinator')} + + {shortNpub(coordinatorPubkey)} + +
+
+ {t('cordnUi.keyPackage')} + + {keyPackageReady ? t('cordnUi.keyPackageReady') : t('cordnUi.keyPackageMissing')} + +
+ {groupRef && ( +
+ {t('cordnUi.groupRef')} + + {groupRef} + + +
+ )} +
+
+ ); +} + +function PersonLabel({ pubkey, isSelf }: { pubkey: string; isSelf?: boolean }) { + const { t } = useTranslation(); + const { data } = useAuthor(pubkey); + const name = data?.metadata?.display_name || data?.metadata?.name || shortNpub(pubkey); + const picture = data?.metadata?.picture; + const initials = name.replace(/^@/, '').slice(0, 2).toUpperCase(); + + return ( +
+ + + + {initials} + + + + {name} + {isSelf && ({t('cordnUi.you')})} + +
+ ); +} + +function MemberRow({ + pubkey, + role, + isSelf, +}: { + pubkey: string; + role: MemberRole; + isSelf: boolean; +}) { + const { t } = useTranslation(); + const label = + role === 'founder' + ? t('cordnUi.roleFounder') + : role === 'admin' + ? t('cordnUi.roleAdmin') + : t('cordnUi.roleMember'); + return ( +
+ + + {label} + +
+ ); +} + +function RequestRow({ + pubkey, + disabled, + onApprove, +}: { + pubkey: string; + disabled: boolean; + onApprove: () => void; +}) { + const { t } = useTranslation(); + return ( +
+ + +
+ ); +} + function Notice({ icon, text }: { icon: ReactNode; text: string }) { return ( diff --git a/src/hooks/useCordnNative.ts b/src/hooks/useCordnNative.ts index 79c5772..f6a1de2 100644 --- a/src/hooks/useCordnNative.ts +++ b/src/hooks/useCordnNative.ts @@ -54,25 +54,32 @@ export interface UseCordnNativeResult { messages: CordnChatMessage[]; busy: boolean; error: string | null; - /** Send a group message (optionally an image URL, encrypted in the payload). */ - send: (text: string, imageUrl?: string) => Promise; + /** + * Send a group message (optionally an image URL, encrypted in the payload). + * Returns the stored message on success, or `null` when the send failed. + */ + send: (text: string, imageUrl?: string) => Promise; /** Re-fetch messages (and adopt any new commits). */ refresh: () => Promise; /** Founder/admin: create the crew's MLS group. */ - createChat: () => Promise; - /** Non-admin: ask the coordinator to be added. */ - requestJoin: () => Promise; + createChat: () => Promise; + /** Non-admin: ask the coordinator to be added (publishes a key package). */ + requestJoin: () => Promise; /** Non-admin: collect a waiting welcome and join. */ checkInvite: () => Promise; /** Founder/admin: pending join requests for this group. */ requests: JoinRequest[]; loadRequests: () => Promise; /** Founder/admin: add a requester (by pubkey) and deliver a welcome. */ - approve: (identifier: string) => Promise; + approve: (identifier: string) => Promise; /** True for interactive signers (Amber) — manual refresh/send, no polling. */ interactiveHintVisible: boolean; /** Shareable `cordn1…` reference for the crew's group (once known). */ groupRef: string | null; + /** True when this device is set up: it holds the group or a published key package. */ + keyPackageReady: boolean; + /** Re-open the session after a connection error (the Retry button). */ + retry: () => void; } const POLL_MS = 6000; @@ -115,9 +122,17 @@ export function useCordnNative({ const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [groupRef, setGroupRef] = useState(null); + const [keyPackageReady, setKeyPackageReady] = useState(false); + const [reloadKey, setReloadKey] = useState(0); const interactive = isInteractiveSigner(user, logins); + const retry = useCallback(() => { + setError(null); + setReady(false); + setReloadKey((n) => n + 1); + }, []); + // Stable primitives for the session effect — depending on the `user` object // itself would re-open the session on every render if its identity churns. const pubkey = user?.pubkey ?? null; @@ -127,6 +142,7 @@ export function useCordnNative({ useEffect(() => { if (!enabled || !pubkey || !signer || !coordinator || !groupId) return; let cancelled = false; + setError(null); const session = import('@/lib/cordn/native').then((mod) => { if (!cancelled) { try { @@ -158,6 +174,10 @@ export function useCordnNative({ const found = opened.listGroups().find((g) => g.gid === groupId) ?? null; setGroup(found); setMessages(found ? opened.messages(found.gid) : []); + setKeyPackageReady( + !!found || + opened.listKeyPackages().some((kp) => kp.publishedAt !== undefined && !kp.consumed), + ); setReady(true); }) .catch((err) => { @@ -169,9 +189,10 @@ export function useCordnNative({ sessionRef.current = null; setReady(false); setGroupRef(null); + setKeyPackageReady(false); if (s) void s.disconnect(); }; - }, [enabled, pubkey, signer, coordinator, groupId]); + }, [enabled, pubkey, signer, coordinator, groupId, reloadKey]); const refresh = useCallback(async () => { const session = sessionRef.current; @@ -213,31 +234,40 @@ export function useCordnNative({ async (text: string, imageUrl?: string) => { const body = text.trim(); const session = sessionRef.current; - if ((!body && !imageUrl) || !session || !group) return; - await run(async () => { - await session.send(group.gid, body, imageUrl); + if ((!body && !imageUrl) || !session || !group) return null; + const stored = await run(async () => { + const message = await session.send(group.gid, body, imageUrl); setMessages(session.messages(group.gid)); + return message; }); + return stored ?? null; }, [group, run], ); const createChat = useCallback(async () => { const session = sessionRef.current; - if (!session || !crew || !groupId) return; - await run(async () => { - const created = await session.createGroup({ groupId, name: crew.name }); - setGroup(created); - setMessages(session.messages(created.gid)); + if (!session || !crew || !groupId) return false; + const created = await run(async () => { + const groupInfo = await session.createGroup({ groupId, name: crew.name }); + setGroup(groupInfo); + setMessages(session.messages(groupInfo.gid)); + return true; }); + return created === true; }, [crew, groupId, run]); const requestJoin = useCallback(async () => { const session = sessionRef.current; - if (!session || !groupId) return; - await run(async () => { + if (!session || !groupId) return false; + const requested = await run(async () => { await session.requestJoin(groupId); + setKeyPackageReady( + session.listKeyPackages().some((kp) => kp.publishedAt !== undefined && !kp.consumed), + ); + return true; }); + return requested === true; }, [groupId, run]); const checkInvite = useCallback(async () => { @@ -246,10 +276,23 @@ export function useCordnNative({ const joined = await run(async () => { const welcomes = await session.fetchWelcomes(); if (welcomes.length === 0) return false; - const joinedGroup = await session.acceptWelcome(welcomes[0], crew.name); - setGroup(joinedGroup); - setMessages(session.messages(joinedGroup.gid)); - return true; + // A signed-in account can have welcomes for several crews. Accept each + // one (so it isn't processed again) but only adopt the group that belongs + // to this crew — otherwise opening crew A could show crew B's chat. + for (const welcome of welcomes) { + try { + const joinedGroup = await session.acceptWelcome(welcome, crew.name); + if (joinedGroup.gid === groupId) { + setGroup(joinedGroup); + setMessages(session.messages(joinedGroup.gid)); + setKeyPackageReady(true); + return true; + } + } catch { + // Welcome for a key package this device no longer has — skip it. + } + } + return false; }); return joined ?? false; }, [crew, groupId, run]); @@ -266,12 +309,14 @@ export function useCordnNative({ const approve = useCallback( async (identifier: string) => { const session = sessionRef.current; - if (!session || !group) return; - await run(async () => { + if (!session || !group) return false; + const approved = await run(async () => { await session.addMemberByIdentifier(group.gid, identifier); const pending = await session.fetchJoinRequests(group.gid); setRequests(pending); + return true; }); + return approved === true; }, [group, run], ); @@ -294,6 +339,8 @@ export function useCordnNative({ approve, interactiveHintVisible: interactive, groupRef, + keyPackageReady, + retry, }; } diff --git a/src/i18n/ca/batch1.ts b/src/i18n/ca/batch1.ts index 2d695e2..e9e91b0 100644 --- a/src/i18n/ca/batch1.ts +++ b/src/i18n/ca/batch1.ts @@ -944,7 +944,37 @@ const batch1 = { "bannerCta": "Configura-ho", "ready": "Connectat · MLS", "errorPrefix": "No s'ha pogut connectar amb el coordinador", - "interactiveHint": "El teu signador demana aprovació a cada acció: fes servir Actualitza i Envia quan calgui." + "interactiveHint": "El teu signador demana aprovació a cada acció: fes servir Actualitza i Envia quan calgui.", + setupDevice: "Configura aquest dispositiu", + setupDeviceBody: "Això dóna a aquest dispositiu una clau pròpia per al xat privat. Un fundador l’aprova una vegada.", + waitingApproval: "S’està esperant que el fundador ho aprovi…", + waitingApprovalManual: "S’està esperant el fundador. Toca “Comprova si hi ha invitació” quan ho aprovi.", + onboardingFounder1: "Inicia el xat privat.", + onboardingFounder2: "Aprova els membres que demanin d’unir-s’hi.", + onboardingFounder3: "Xateja en privat amb la teva colla.", + onboardingMember1: "Demana d'unir-t'hi", + onboardingMember2: "El fundador aprova el teu dispositiu.", + onboardingMember3: "Ja hi ets: els missatges van xifrats.", + encryptedNote: "Només ho poden llegir els membres de la colla; el coordinador només retransmet missatges xifrats.", + advanced: "Avançat", + coordinator: "Coordinador", + keyPackage: "Paquet de claus", + keyPackageReady: "A punt en aquest dispositiu", + keyPackageMissing: "No configurat en aquest dispositiu", + members: "Membres", + roleFounder: "Fundador", + roleAdmin: "Administrador", + roleMember: "Membre", + qrTitle: "Escaneja per unir-te al xat privat", + qrHint: "Els membres de la colla ho escanegen amb la càmera del mòbil. L’enllaç obre la pestanya Xat privat.", + sendFailed: "No s’ha pogut enviar: toca Reintenta.", + sending: "S’està enviant…", + sent: "Enviat", + retry: "Reintenta", + errorUnreachable: "No s’ha pogut connectar amb el xat privat. Comprova la connexió.", + errorDetails: "Detalls", + unreachable: "Inabastable", + dismiss: "Tanca", }, "trustUi": { "alertsTitle": "Alertes de la xarxa", diff --git a/src/i18n/cs/drift.ts b/src/i18n/cs/drift.ts index 33f4b2e..5b8f5e2 100644 --- a/src/i18n/cs/drift.ts +++ b/src/i18n/cs/drift.ts @@ -322,7 +322,37 @@ const drift = { "bannerCta": "Nastavit", "ready": "Připojeno · MLS", "errorPrefix": "Koordinátora se nepodařilo kontaktovat", - "interactiveHint": "Tvůj signer žádá o schválení u každé akce — podle potřeby použij Obnovit a Poslat." + "interactiveHint": "Tvůj signer žádá o schválení u každé akce — podle potřeby použij Obnovit a Poslat.", + setupDevice: "Nastavit toto zařízení", + setupDeviceBody: "Toto zařízení dostane vlastní klíč pro soukromý chat. Zakladatel ho jednou schválí.", + waitingApproval: "Čeká se na schválení zakladatelem…", + waitingApprovalManual: "Čeká se na zakladatele. Až schválí, klepni na „Zkontrolovat pozvánku“.", + onboardingFounder1: "Zahaj soukromý chat.", + onboardingFounder2: "Schvaluj členy, kteří požádají o připojení.", + onboardingFounder3: "Chatuj soukromě se svou crew.", + onboardingMember1: "Požádat o připojení", + onboardingMember2: "Zakladatel schválí tvé zařízení.", + onboardingMember3: "Jsi uvnitř — zprávy jsou šifrované.", + encryptedNote: "Číst to mohou jen členové crew; koordinátor pouze přenáší šifrované zprávy.", + advanced: "Pokročilé", + coordinator: "Koordinátor", + keyPackage: "Balíček klíčů", + keyPackageReady: "Připraveno na tomto zařízení", + keyPackageMissing: "Není nastaveno na tomto zařízení", + members: "Členové", + roleFounder: "Zakladatel", + roleAdmin: "Administrátor", + roleMember: "Člen", + qrTitle: "Naskenuj a připoj se k soukromému chatu", + qrHint: "Členové crew to naskenují kamerou telefonu. Odkaz otevře kartu Soukromý chat.", + sendFailed: "Nepodařilo se odeslat — klepni na Zkusit znovu.", + sending: "Odesílání…", + sent: "Odesláno", + retry: "Zkusit znovu", + errorUnreachable: "Soukromý chat se nepodařilo kontaktovat. Zkontroluj připojení.", + errorDetails: "Podrobnosti", + unreachable: "Nedostupné", + dismiss: "Zavřít", }, translateUi: { title: "Překlad zpráv", diff --git a/src/i18n/de/drift.ts b/src/i18n/de/drift.ts index 85a31af..0d39394 100644 --- a/src/i18n/de/drift.ts +++ b/src/i18n/de/drift.ts @@ -325,6 +325,36 @@ const drift = { ready: "Verbunden · MLS", errorPrefix: "Koordinator konnte nicht erreicht werden", interactiveHint: "Dein Signer fragt bei jeder Aktion nach Bestätigung – nutze Aktualisieren und Senden nach Bedarf.", + setupDevice: "Dieses Gerät einrichten", + setupDeviceBody: "Dieses Gerät erhält einen eigenen Schlüssel für den privaten Chat. Ein Gründer genehmigt ihn einmal.", + waitingApproval: "Warte auf die Genehmigung durch den Gründer…", + waitingApprovalManual: "Warte auf den Gründer. Tippe nach der Genehmigung auf „Nach Einladung suchen“.", + onboardingFounder1: "Starte den privaten Chat.", + onboardingFounder2: "Genehmige Mitglieder, die um Beitritt bitten.", + onboardingFounder3: "Chatte privat mit deiner Crew.", + onboardingMember1: "Um Beitritt bitten", + onboardingMember2: "Der Gründer genehmigt dein Gerät.", + onboardingMember3: "Du bist dabei – Nachrichten sind verschlüsselt.", + encryptedNote: "Nur Crew-Mitglieder können das lesen; der Koordinator leitet nur verschlüsselte Nachrichten weiter.", + advanced: "Erweitert", + coordinator: "Koordinator", + keyPackage: "Schlüsselpaket", + keyPackageReady: "Auf diesem Gerät bereit", + keyPackageMissing: "Auf diesem Gerät nicht eingerichtet", + members: "Mitglieder", + roleFounder: "Gründer", + roleAdmin: "Admin", + roleMember: "Mitglied", + qrTitle: "Scannen und dem privaten Chat beitreten", + qrHint: "Crew-Mitglieder scannen das mit der Handykamera. Der Link öffnet den Tab „Privater Chat“.", + sendFailed: "Senden fehlgeschlagen – tippe auf Wiederholen.", + sending: "Senden…", + sent: "Gesendet", + retry: "Wiederholen", + errorUnreachable: "Der private Chat ist nicht erreichbar. Prüfe deine Verbindung.", + errorDetails: "Details", + unreachable: "Nicht erreichbar", + dismiss: "Schließen", }, translateUi: { title: "Nachrichtenübersetzung", diff --git a/src/i18n/el/drift.ts b/src/i18n/el/drift.ts index 857138f..2f3db35 100644 --- a/src/i18n/el/drift.ts +++ b/src/i18n/el/drift.ts @@ -327,6 +327,36 @@ const drift = { ready: "Συνδεδεμένο · MLS", errorPrefix: "Δεν ήταν δυνατή η πρόσβαση στον συντονιστή", interactiveHint: "Ο signer σου ζητά έγκριση για κάθε ενέργεια — χρησιμοποίησε Ανανέωση και Αποστολή όπως χρειάζεται.", + setupDevice: "Ρύθμιση αυτής της συσκευής", + setupDeviceBody: "Αυτό δίνει στη συσκευή το δικό της κλειδί για την ιδιωτική συνομιλία. Ένας ιδρυτής το εγκρίνει μία φορά.", + waitingApproval: "Αναμονή έγκρισης από τον ιδρυτή…", + waitingApprovalManual: "Αναμονή για τον ιδρυτή. Πατήστε «Έλεγχος πρόσκλησης» αφού εγκρίνει.", + onboardingFounder1: "Ξεκινήστε την ιδιωτική συνομιλία.", + onboardingFounder2: "Εγκρίνετε τα μέλη που ζητούν συμμετοχή.", + onboardingFounder3: "Συνομιλήστε ιδιωτικά με το crew σας.", + onboardingMember1: "Ζήτα συμμετοχή", + onboardingMember2: "Ο ιδρυτής εγκρίνει τη συσκευή σας.", + onboardingMember3: "Είστε μέσα — τα μηνύματα είναι κρυπτογραφημένα.", + encryptedNote: "Μόνο τα μέλη του crew μπορούν να το διαβάσουν· ο συντονιστής απλώς προωθεί κρυπτογραφημένα μηνύματα.", + advanced: "Για προχωρημένους", + coordinator: "Συντονιστής", + keyPackage: "Πακέτο κλειδιών", + keyPackageReady: "Έτοιμο σε αυτή τη συσκευή", + keyPackageMissing: "Δεν έχει ρυθμιστεί σε αυτή τη συσκευή", + members: "Μέλη", + roleFounder: "Ιδρυτής", + roleAdmin: "Διαχειριστής", + roleMember: "Μέλος", + qrTitle: "Σαρώστε για να μπείτε στην ιδιωτική συνομιλία", + qrHint: "Τα μέλη του crew το σαρώνουν με την κάμερα του κινητού. Ο σύνδεσμος ανοίγει την καρτέλα Ιδιωτική συνομιλία.", + sendFailed: "Δεν στάλθηκε — πατήστε Επανάληψη.", + sending: "Αποστολή…", + sent: "Στάλθηκε", + retry: "Επανάληψη", + errorUnreachable: "Δεν ήταν δυνατή η πρόσβαση στην ιδιωτική συνομιλία. Ελέγξτε τη σύνδεσή σας.", + errorDetails: "Λεπτομέρειες", + unreachable: "Μη προσβάσιμο", + dismiss: "Κλείσιμο", }, translateUi: { title: "Μετάφραση μηνυμάτων", diff --git a/src/i18n/eu/batch1.ts b/src/i18n/eu/batch1.ts index 7245d0d..8cdaf5a 100644 --- a/src/i18n/eu/batch1.ts +++ b/src/i18n/eu/batch1.ts @@ -944,7 +944,37 @@ const batch1 = { bannerCta: "Konfiguratu", ready: "Konektatuta · MLS", errorPrefix: "Ezin izan da koordinatzailera iritsi", - interactiveHint: "Zure sinatzaileak ekintza bakoitzean onarpena eskatzen du — erabili Freskatu eta Bidali behar duzunean." + interactiveHint: "Zure sinatzaileak ekintza bakoitzean onarpena eskatzen du — erabili Freskatu eta Bidali behar duzunean.", + setupDevice: "Konfiguratu gailu hau", + setupDeviceBody: "Honek gailu honi txat pribaturako bere gakoa ematen dio. Sortzaile batek behin onartzen du.", + waitingApproval: "Sortzailearen onarpenaren zain…", + waitingApprovalManual: "Sortzailearen zain. Onartu ondoren, sakatu “Egiaztatu gonbidapena”.", + onboardingFounder1: "Hasi txat pribatua.", + onboardingFounder2: "Onartu sartzea eskatzen duten kideak.", + onboardingFounder3: "Txateatu pribatuan zure taldearekin.", + onboardingMember1: "Eskatu sartzea", + onboardingMember2: "Sortzaileak zure gailua onartzen du.", + onboardingMember3: "Barruan zaude — mezuak zifratuta daude.", + encryptedNote: "Taldeko kideek soilik irakur dezakete hau; koordinatzaileak mezu zifratuak soilik birbidaltzen ditu.", + advanced: "Aurreratua", + coordinator: "Koordinatzailea", + keyPackage: "Gako-paketea", + keyPackageReady: "Prest gailu honetan", + keyPackageMissing: "Ez dago konfiguratuta gailu honetan", + members: "Kideak", + roleFounder: "Sortzailea", + roleAdmin: "Administratzailea", + roleMember: "Kidea", + qrTitle: "Eskaneatu txat pribatura sartzeko", + qrHint: "Taldeko kideek telefonoaren kamerarekin eskaneatzen dute. Estekak Txat pribatua fitxa irekitzen du.", + sendFailed: "Ezin izan da bidali — sakatu Berriro saiatu.", + sending: "Bidaltzen…", + sent: "Bidalita", + retry: "Berriro saiatu", + errorUnreachable: "Ezin izan da txat pribatura iritsi. Egiaztatu zure konexioa.", + errorDetails: "Xehetasunak", + unreachable: "Ezin iritsi", + dismiss: "Itxi", }, trustUi: { alertsTitle: "Sareko alertak", diff --git a/src/i18n/fr/drift.ts b/src/i18n/fr/drift.ts index 7ee4020..945d2d5 100644 --- a/src/i18n/fr/drift.ts +++ b/src/i18n/fr/drift.ts @@ -325,6 +325,36 @@ const drift = { ready: "Connecté · MLS", errorPrefix: "Impossible de joindre le coordinateur", interactiveHint: "Votre signataire demande une approbation à chaque action — utilisez Actualiser et Envoyer au besoin.", + setupDevice: "Configurer cet appareil", + setupDeviceBody: "Cet appareil obtient sa propre clé pour le chat privé. Un fondateur l’approuve une fois.", + waitingApproval: "En attente de l’approbation du fondateur…", + waitingApprovalManual: "En attente du fondateur. Touchez « Vérifier l’invitation » après son approbation.", + onboardingFounder1: "Lancez le chat privé.", + onboardingFounder2: "Approuvez les membres qui demandent à rejoindre.", + onboardingFounder3: "Discutez en privé avec votre Crew.", + onboardingMember1: "Demander à rejoindre", + onboardingMember2: "Le fondateur approuve votre appareil.", + onboardingMember3: "Vous y êtes — les messages sont chiffrés.", + encryptedNote: "Seuls les membres de la Crew peuvent le lire ; le coordinateur ne relaie que des messages chiffrés.", + advanced: "Avancé", + coordinator: "Coordinateur", + keyPackage: "Paquet de clés", + keyPackageReady: "Prêt sur cet appareil", + keyPackageMissing: "Non configuré sur cet appareil", + members: "Membres", + roleFounder: "Fondateur", + roleAdmin: "Admin", + roleMember: "Membre", + qrTitle: "Scannez pour rejoindre le chat privé", + qrHint: "Les membres de la Crew le scannent avec l’appareil photo de leur téléphone. Le lien ouvre l’onglet Chat privé.", + sendFailed: "Échec de l’envoi — touchez Réessayer.", + sending: "Envoi…", + sent: "Envoyé", + retry: "Réessayer", + errorUnreachable: "Impossible de joindre le chat privé. Vérifiez votre connexion.", + errorDetails: "Détails", + unreachable: "Injoignable", + dismiss: "Fermer", }, translateUi: { title: "Traduction des messages", diff --git a/src/i18n/gl/batch1.ts b/src/i18n/gl/batch1.ts index 0135751..243bb73 100644 --- a/src/i18n/gl/batch1.ts +++ b/src/i18n/gl/batch1.ts @@ -944,7 +944,37 @@ const batch1 = { bannerCta: "Configurar", ready: "Conectado · MLS", errorPrefix: "Non se puido conectar co coordinador", - interactiveHint: "O teu firmante pide aprobación en cada acción: usa Actualizar e Enviar cando faga falta." + interactiveHint: "O teu firmante pide aprobación en cada acción: usa Actualizar e Enviar cando faga falta.", + setupDevice: "Configurar este dispositivo", + setupDeviceBody: "Isto dálle a este dispositivo unha chave propia para o chat privado. Un fundador aprobaa unha vez.", + waitingApproval: "Agardando a aprobación do fundador…", + waitingApprovalManual: "Agardando polo fundador. Toca «Comprobar se hai invitación» cando aprobe.", + onboardingFounder1: "Inicia o chat privado.", + onboardingFounder2: "Aproba os membros que pidan acceder.", + onboardingFounder3: "Chatea en privado coa túa crew.", + onboardingMember1: "Pedir acceso", + onboardingMember2: "O fundador aproba o teu dispositivo.", + onboardingMember3: "Xa estás dentro: as mensaxes van cifradas.", + encryptedNote: "Só os membros da crew poden ler isto; o coordinador só retransmite mensaxes cifradas.", + advanced: "Avanzado", + coordinator: "Coordinador", + keyPackage: "Paquete de chaves", + keyPackageReady: "Listo neste dispositivo", + keyPackageMissing: "Sen configurar neste dispositivo", + members: "Membros", + roleFounder: "Fundador", + roleAdmin: "Administrador", + roleMember: "Membro", + qrTitle: "Escanea para entrar no chat privado", + qrHint: "Os membros da crew escanéano coa cámara do móbil. A ligazón abre a pestana Chat privado.", + sendFailed: "Non se puido enviar: toca Reintentar.", + sending: "Enviando…", + sent: "Enviado", + retry: "Reintentar", + errorUnreachable: "Non se puido conectar co chat privado. Comproba a túa conexión.", + errorDetails: "Detalles", + unreachable: "Inaccesible", + dismiss: "Pechar", }, trustUi: { alertsTitle: "Avisos da rede", diff --git a/src/i18n/hi/batch1.ts b/src/i18n/hi/batch1.ts index eafe1c9..32c6de6 100644 --- a/src/i18n/hi/batch1.ts +++ b/src/i18n/hi/batch1.ts @@ -944,7 +944,37 @@ const batch1 = { "bannerCta": "सेट करें", "ready": "कनेक्टेड · MLS", "errorPrefix": "कोऑर्डिनेटर तक नहीं पहुँच सके", - "interactiveHint": "आपका साइनर हर क्रिया पर स्वीकृति माँगता है — ज़रूरत के अनुसार रीफ़्रेश और भेजें का उपयोग करें।" + "interactiveHint": "आपका साइनर हर क्रिया पर स्वीकृति माँगता है — ज़रूरत के अनुसार रीफ़्रेश और भेजें का उपयोग करें।", + setupDevice: "इस डिवाइस को सेट करें", + setupDeviceBody: "यह इस डिवाइस को निजी चैट के लिए अपनी कुंजी देता है। एक संस्थापक इसे एक बार स्वीकृत करता है।", + waitingApproval: "संस्थापक की स्वीकृति की प्रतीक्षा हो रही है…", + waitingApprovalManual: "संस्थापक की प्रतीक्षा है। स्वीकृति के बाद “निमंत्रण जाँचें” टैप करें।", + onboardingFounder1: "निजी चैट शुरू करें।", + onboardingFounder2: "शामिल होने का अनुरोध करने वाले सदस्यों को स्वीकृत करें।", + onboardingFounder3: "अपने क्रू के साथ निजी में चैट करें।", + onboardingMember1: "शामिल होने का अनुरोध करें", + onboardingMember2: "संस्थापक आपका डिवाइस स्वीकृत करता है।", + onboardingMember3: "आप अंदर हैं — संदेश एन्क्रिप्टेड हैं।", + encryptedNote: "इसे केवल क्रू सदस्य ही पढ़ सकते हैं; कोऑर्डिनेटर केवल एन्क्रिप्टेड संदेश आगे भेजता है।", + advanced: "उन्नत", + coordinator: "कोऑर्डिनेटर", + keyPackage: "कुंजी पैकेज", + keyPackageReady: "इस डिवाइस पर तैयार", + keyPackageMissing: "इस डिवाइस पर सेट नहीं है", + members: "सदस्य", + roleFounder: "संस्थापक", + roleAdmin: "एडमिन", + roleMember: "सदस्य", + qrTitle: "निजी चैट में शामिल होने के लिए स्कैन करें", + qrHint: "क्रू सदस्य इसे फ़ोन कैमरे से स्कैन करते हैं। लिंक निजी चैट टैब खोलता है।", + sendFailed: "भेजा नहीं जा सका — रीट्राई टैप करें।", + sending: "भेजा जा रहा है…", + sent: "भेज दिया", + retry: "रीट्राई", + errorUnreachable: "निजी चैट तक नहीं पहुँच सके। अपना कनेक्शन जाँचें।", + errorDetails: "विवरण", + unreachable: "पहुँच से बाहर", + dismiss: "बंद करें", }, "trustUi": { "alertsTitle": "नेटवर्क अलर्ट", diff --git a/src/i18n/id/drift.ts b/src/i18n/id/drift.ts index 776ef81..10a5a50 100644 --- a/src/i18n/id/drift.ts +++ b/src/i18n/id/drift.ts @@ -319,6 +319,36 @@ const driftStrings = { ready: 'Terhubung · MLS', errorPrefix: 'Tidak dapat menjangkau koordinator', interactiveHint: 'Penandatanganmu meminta persetujuan untuk setiap tindakan — gunakan Muat ulang dan Kirim sesuai kebutuhan.', + setupDevice: "Siapkan perangkat ini", + setupDeviceBody: "Ini memberi perangkat ini kunci sendiri untuk chat privat. Seorang pendiri menyetujuinya sekali.", + waitingApproval: "Menunggu persetujuan pendiri…", + waitingApprovalManual: "Menunggu pendiri. Ketuk “Periksa undangan” setelah mereka menyetujui.", + onboardingFounder1: "Mulai chat privat.", + onboardingFounder2: "Setujui anggota yang minta gabung.", + onboardingFounder3: "Ngobrol secara privat dengan crew-mu.", + onboardingMember1: 'Minta gabung', + onboardingMember2: "Pendiri menyetujui perangkatmu.", + onboardingMember3: "Kamu sudah masuk — pesan terenkripsi.", + encryptedNote: "Hanya anggota crew yang bisa membacanya; koordinator hanya meneruskan pesan terenkripsi.", + advanced: "Lanjutan", + coordinator: "Koordinator", + keyPackage: "Paket kunci", + keyPackageReady: "Siap di perangkat ini", + keyPackageMissing: "Belum disiapkan di perangkat ini", + members: "Anggota", + roleFounder: "Pendiri", + roleAdmin: "Admin", + roleMember: "Anggota", + qrTitle: "Pindai untuk bergabung ke chat privat", + qrHint: "Anggota crew memindai ini dengan kamera ponsel. Tautan membuka tab Chat privat.", + sendFailed: "Gagal mengirim — ketuk Coba lagi.", + sending: "Mengirim…", + sent: "Terkirim", + retry: "Coba lagi", + errorUnreachable: "Tidak dapat menjangkau chat privat. Periksa koneksimu.", + errorDetails: "Detail", + unreachable: "Tidak terjangkau", + dismiss: "Tutup", }, translateUi: { title: 'Terjemahan pesan', diff --git a/src/i18n/it/drift.ts b/src/i18n/it/drift.ts index 42573b7..92ccc2f 100644 --- a/src/i18n/it/drift.ts +++ b/src/i18n/it/drift.ts @@ -325,6 +325,36 @@ const drift = { ready: "Connesso · MLS", errorPrefix: "Impossibile raggiungere il coordinatore", interactiveHint: "Il tuo signer chiede l'approvazione a ogni azione — usa Aggiorna e Invia secondo necessità.", + setupDevice: "Configura questo dispositivo", + setupDeviceBody: "Questo assegna al dispositivo una chiave propria per la chat privata. Un fondatore la approva una volta.", + waitingApproval: "In attesa dell’approvazione del fondatore…", + waitingApprovalManual: "In attesa del fondatore. Tocca «Controlla l’invito» dopo l’approvazione.", + onboardingFounder1: "Avvia la chat privata.", + onboardingFounder2: "Approva i membri che chiedono di unirsi.", + onboardingFounder3: "Chatta in privato con la tua crew.", + onboardingMember1: "Chiedi di unirti", + onboardingMember2: "Il fondatore approva il tuo dispositivo.", + onboardingMember3: "Sei dentro — i messaggi sono cifrati.", + encryptedNote: "Solo i membri della crew possono leggerlo; il coordinatore inoltra solo messaggi cifrati.", + advanced: "Avanzate", + coordinator: "Coordinatore", + keyPackage: "Pacchetto di chiavi", + keyPackageReady: "Pronto su questo dispositivo", + keyPackageMissing: "Non configurato su questo dispositivo", + members: "Membri", + roleFounder: "Fondatore", + roleAdmin: "Admin", + roleMember: "Membro", + qrTitle: "Scansiona per unirti alla chat privata", + qrHint: "I membri della crew lo scansionano con la fotocamera del telefono. Il link apre la scheda Chat privata.", + sendFailed: "Invio non riuscito — tocca Riprova.", + sending: "Invio…", + sent: "Inviato", + retry: "Riprova", + errorUnreachable: "Impossibile raggiungere la chat privata. Controlla la connessione.", + errorDetails: "Dettagli", + unreachable: "Non raggiungibile", + dismiss: "Chiudi", }, translateUi: { title: "Traduzione dei messaggi", diff --git a/src/i18n/ja/drift.ts b/src/i18n/ja/drift.ts index f983094..89a751c 100644 --- a/src/i18n/ja/drift.ts +++ b/src/i18n/ja/drift.ts @@ -335,6 +335,36 @@ const drift: Dict = { ready: '接続済み · MLS', errorPrefix: 'コーディネーターに接続できませんでした', interactiveHint: '署名者は各操作で承認を求めます — 必要に応じて更新と送信を使ってください。', + setupDevice: "このデバイスを設定", + setupDeviceBody: "このデバイス専用のキーをプライベートチャットに付与します。創設者が一度承認します。", + waitingApproval: "創設者の承認を待っています…", + waitingApprovalManual: "創設者を待っています。承認されたら「招待を確認」をタップしてください。", + onboardingFounder1: "プライベートチャットを開始しましょう。", + onboardingFounder2: "参加を希望するメンバーを承認しましょう。", + onboardingFounder3: "クルーとプライベートにチャットしましょう。", + onboardingMember1: '参加をリクエスト', + onboardingMember2: "創設者があなたのデバイスを承認します。", + onboardingMember3: "参加しました — メッセージは暗号化されています。", + encryptedNote: "読めるのはクルーメンバーだけです。コーディネーターは暗号化されたメッセージを中継するだけです。", + advanced: "詳細設定", + coordinator: "コーディネーター", + keyPackage: "キーパッケージ", + keyPackageReady: "このデバイスで準備完了", + keyPackageMissing: "このデバイスでは未設定", + members: "メンバー", + roleFounder: "創設者", + roleAdmin: "管理者", + roleMember: "メンバー", + qrTitle: "スキャンしてプライベートチャットに参加", + qrHint: "クルーメンバーがスマートフォンのカメラでスキャンします。リンクからプライベートチャットタブが開きます。", + sendFailed: "送信できませんでした — 「再試行」をタップしてください。", + sending: "送信中…", + sent: "送信済み", + retry: "再試行", + errorUnreachable: "プライベートチャットに接続できませんでした。接続を確認してください。", + errorDetails: "詳細", + unreachable: "接続不可", + dismiss: "閉じる", }, translateUi: { title: 'メッセージの翻訳', diff --git a/src/i18n/ko/drift.ts b/src/i18n/ko/drift.ts index 3a09dd0..e1d95f7 100644 --- a/src/i18n/ko/drift.ts +++ b/src/i18n/ko/drift.ts @@ -335,6 +335,36 @@ const drift: Dict = { ready: '연결됨 · MLS', errorPrefix: '코디네이터에 연결할 수 없습니다', interactiveHint: '서명자가 각 작업마다 승인을 요청합니다 — 필요에 따라 새로고침과 보내기를 사용하세요.', + setupDevice: "이 기기 설정", + setupDeviceBody: "이 기기에 비공개 채팅용 키를 부여합니다. 창립자가 한 번 승인합니다.", + waitingApproval: "창립자의 승인을 기다리는 중…", + waitingApprovalManual: "창립자를 기다리는 중입니다. 승인되면 “초대 확인”을 누르세요.", + onboardingFounder1: "비공개 채팅을 시작하세요.", + onboardingFounder2: "참여를 요청한 멤버를 승인하세요.", + onboardingFounder3: "크루와 비공개로 대화하세요.", + onboardingMember1: '참여 요청', + onboardingMember2: "창립자가 기기를 승인합니다.", + onboardingMember3: "참여했습니다 — 메시지는 암호화됩니다.", + encryptedNote: "크루 멤버만 읽을 수 있으며, 코디네이터는 암호화된 메시지만 중계합니다.", + advanced: "고급", + coordinator: "코디네이터", + keyPackage: "키 패키지", + keyPackageReady: "이 기기에서 준비됨", + keyPackageMissing: "이 기기에서 설정되지 않음", + members: "멤버", + roleFounder: "창립자", + roleAdmin: "관리자", + roleMember: "멤버", + qrTitle: "스캔하여 비공개 채팅 참여", + qrHint: "크루 멤버가 휴대폰 카메라로 스캔합니다. 링크를 열면 비공개 채팅 탭으로 이동합니다.", + sendFailed: "보내지 못했습니다 — 다시 시도를 누르세요.", + sending: "보내는 중…", + sent: "보냄", + retry: "다시 시도", + errorUnreachable: "비공개 채팅에 연결할 수 없습니다. 연결을 확인하세요.", + errorDetails: "세부정보", + unreachable: "연결 불가", + dismiss: "닫기", }, translateUi: { title: '메시지 번역', diff --git a/src/i18n/legacy/cordnUi.ts b/src/i18n/legacy/cordnUi.ts index 6a2ee49..d64526c 100644 --- a/src/i18n/legacy/cordnUi.ts +++ b/src/i18n/legacy/cordnUi.ts @@ -45,6 +45,40 @@ const cordnUi = { errorPrefix: 'Couldn’t reach the coordinator', interactiveHint: 'Your signer asks for approval on each action — use Refresh and Send as needed.', + setupDevice: 'Set up this device', + setupDeviceBody: + 'This gives this device its own key for the private chat. A founder approves it once.', + waitingApproval: 'Waiting for the founder to approve…', + waitingApprovalManual: + 'Waiting for the founder. Tap “Check for invite” after they approve.', + onboardingFounder1: 'Start the private chat.', + onboardingFounder2: 'Approve members who ask to join.', + onboardingFounder3: 'Chat privately with your crew.', + onboardingMember1: 'Ask to join.', + onboardingMember2: 'The founder approves your device.', + onboardingMember3: 'You’re in — messages are encrypted.', + encryptedNote: + 'Only crew members can read this; the coordinator only relays encrypted messages.', + advanced: 'Advanced', + coordinator: 'Coordinator', + keyPackage: 'Key package', + keyPackageReady: 'Ready on this device', + keyPackageMissing: 'Not set up on this device', + members: 'Members', + roleFounder: 'Founder', + roleAdmin: 'Admin', + roleMember: 'Member', + qrTitle: 'Scan to join the private chat', + qrHint: + 'Crew members scan this with their phone camera. The link opens the Private chat tab.', + sendFailed: 'Couldn’t send — tap Retry.', + sending: 'Sending…', + sent: 'Sent', + retry: 'Retry', + errorUnreachable: 'Couldn’t reach the private chat. Check your connection.', + errorDetails: 'Details', + unreachable: 'Unreachable', + dismiss: 'Dismiss', }, es: { title: 'Chat privado', @@ -87,6 +121,40 @@ const cordnUi = { errorPrefix: 'No se pudo contactar con el coordinador', interactiveHint: 'Tu firmante pide aprobación en cada acción: usa Actualizar y Enviar cuando haga falta.', + setupDevice: 'Configurar este dispositivo', + setupDeviceBody: + 'Esto da a este dispositivo su propia clave para el chat privado. Un fundador lo aprueba una vez.', + waitingApproval: 'Esperando a que el fundador lo apruebe…', + waitingApprovalManual: + 'Esperando al fundador. Pulsa «Buscar invitación» cuando lo apruebe.', + onboardingFounder1: 'Crea el chat privado.', + onboardingFounder2: 'Aprueba a los miembros que pidan entrar.', + onboardingFounder3: 'Habla en privado con tu peña.', + onboardingMember1: 'Pide entrar.', + onboardingMember2: 'El fundador aprueba tu dispositivo.', + onboardingMember3: 'Ya estás dentro: los mensajes van cifrados.', + encryptedNote: + 'Solo los miembros de la peña pueden leerlo; el coordinador solo retransmite mensajes cifrados.', + advanced: 'Avanzado', + coordinator: 'Coordinador', + keyPackage: 'Paquete de claves', + keyPackageReady: 'Listo en este dispositivo', + keyPackageMissing: 'Sin configurar en este dispositivo', + members: 'Miembros', + roleFounder: 'Fundador', + roleAdmin: 'Admin', + roleMember: 'Miembro', + qrTitle: 'Escanea para entrar al chat privado', + qrHint: + 'Los miembros de la peña pueden escanearlo con la cámara del móvil. El enlace abre la pestaña del chat privado.', + sendFailed: 'No se pudo enviar: pulsa Reintentar.', + sending: 'Enviando…', + sent: 'Enviado', + retry: 'Reintentar', + errorUnreachable: 'No se pudo conectar con el chat privado. Comprueba tu conexión.', + errorDetails: 'Detalles', + unreachable: 'Sin conexión', + dismiss: 'Descartar', }, }; diff --git a/src/i18n/nl/drift.ts b/src/i18n/nl/drift.ts index 28214d0..817fe60 100644 --- a/src/i18n/nl/drift.ts +++ b/src/i18n/nl/drift.ts @@ -322,7 +322,37 @@ const drift = { "bannerCta": "Instellen", "ready": "Verbonden · MLS", "errorPrefix": "Kon de coördinator niet bereiken", - "interactiveHint": "Je signer vraagt goedkeuring bij elke actie — gebruik Vernieuwen en Verzenden zoals nodig." + "interactiveHint": "Je signer vraagt goedkeuring bij elke actie — gebruik Vernieuwen en Verzenden zoals nodig.", + setupDevice: "Dit apparaat instellen", + setupDeviceBody: "Dit geeft dit apparaat een eigen sleutel voor de privéchat. Een oprichter keurt die één keer goed.", + waitingApproval: "Wachten op goedkeuring door de oprichter…", + waitingApprovalManual: "Wachten op de oprichter. Tik na goedkeuring op “Op uitnodiging controleren”.", + onboardingFounder1: "Start de privéchat.", + onboardingFounder2: "Keur leden goed die willen deelnemen.", + onboardingFounder3: "Chat privé met je crew.", + onboardingMember1: "Vragen om deel te nemen", + onboardingMember2: "De oprichter keurt je apparaat goed.", + onboardingMember3: "Je doet mee — berichten zijn versleuteld.", + encryptedNote: "Alleen crewleden kunnen dit lezen; de coördinator geeft alleen versleutelde berichten door.", + advanced: "Geavanceerd", + coordinator: "Coördinator", + keyPackage: "Sleutelpakket", + keyPackageReady: "Klaar op dit apparaat", + keyPackageMissing: "Niet ingesteld op dit apparaat", + members: "Leden", + roleFounder: "Oprichter", + roleAdmin: "Admin", + roleMember: "Lid", + qrTitle: "Scan om deel te nemen aan de privéchat", + qrHint: "Crewleden scannen dit met de camera van hun telefoon. De link opent het tabblad Privéchat.", + sendFailed: "Verzenden mislukt — tik op Opnieuw.", + sending: "Verzenden…", + sent: "Verzonden", + retry: "Opnieuw", + errorUnreachable: "Kon de privéchat niet bereiken. Controleer je verbinding.", + errorDetails: "Details", + unreachable: "Onbereikbaar", + dismiss: "Sluiten", }, translateUi: { title: "Berichtvertaling", diff --git a/src/i18n/pl/drift.ts b/src/i18n/pl/drift.ts index 36b70c3..402c998 100644 --- a/src/i18n/pl/drift.ts +++ b/src/i18n/pl/drift.ts @@ -322,7 +322,37 @@ const drift = { "bannerCta": "Skonfiguruj", "ready": "Połączono · MLS", "errorPrefix": "Nie udało się połączyć z koordynatorem", - "interactiveHint": "Twój signer prosi o zatwierdzenie każdej akcji — używaj Odśwież i Wyślij w razie potrzeby." + "interactiveHint": "Twój signer prosi o zatwierdzenie każdej akcji — używaj Odśwież i Wyślij w razie potrzeby.", + setupDevice: "Skonfiguruj to urządzenie", + setupDeviceBody: "To daje temu urządzeniu własny klucz do prywatnego czatu. Założyciel zatwierdza go raz.", + waitingApproval: "Czekanie na zatwierdzenie przez założyciela…", + waitingApprovalManual: "Czekanie na założyciela. Po zatwierdzeniu dotknij „Sprawdź zaproszenie”.", + onboardingFounder1: "Rozpocznij prywatny czat.", + onboardingFounder2: "Zatwierdzaj członków, którzy proszą o dołączenie.", + onboardingFounder3: "Rozmawiaj prywatnie ze swoim crew.", + onboardingMember1: "Poproś o dołączenie", + onboardingMember2: "Założyciel zatwierdza twoje urządzenie.", + onboardingMember3: "Jesteś w środku — wiadomości są szyfrowane.", + encryptedNote: "Mogą to czytać tylko członkowie crew; koordynator przekazuje wyłącznie zaszyfrowane wiadomości.", + advanced: "Zaawansowane", + coordinator: "Koordynator", + keyPackage: "Pakiet kluczy", + keyPackageReady: "Gotowe na tym urządzeniu", + keyPackageMissing: "Nieskonfigurowane na tym urządzeniu", + members: "Członkowie", + roleFounder: "Założyciel", + roleAdmin: "Administrator", + roleMember: "Członek", + qrTitle: "Zeskanuj, aby dołączyć do prywatnego czatu", + qrHint: "Członkowie crew skanują to aparatem telefonu. Link otwiera kartę Prywatny czat.", + sendFailed: "Nie udało się wysłać — dotknij Ponów.", + sending: "Wysyłanie…", + sent: "Wysłano", + retry: "Ponów", + errorUnreachable: "Nie udało się połączyć z prywatnym czatem. Sprawdź połączenie.", + errorDetails: "Szczegóły", + unreachable: "Nieosiągalne", + dismiss: "Zamknij", }, translateUi: { title: "Tłumaczenie wiadomości", diff --git a/src/i18n/pt/drift.ts b/src/i18n/pt/drift.ts index d9e6bb7..ed69b33 100644 --- a/src/i18n/pt/drift.ts +++ b/src/i18n/pt/drift.ts @@ -325,6 +325,36 @@ const drift = { ready: "Conectado · MLS", errorPrefix: "Não foi possível alcançar o coordenador", interactiveHint: "Seu signer pede aprovação a cada ação — use Atualizar e Enviar conforme necessário.", + setupDevice: "Configurar este dispositivo", + setupDeviceBody: "Isto dá a este dispositivo uma chave própria para o chat privado. Um fundador aprova uma vez.", + waitingApproval: "Aguardando aprovação do fundador…", + waitingApprovalManual: "Aguardando o fundador. Toque em “Verificar convite” depois que ele aprovar.", + onboardingFounder1: "Inicie o chat privado.", + onboardingFounder2: "Aprove os membros que pedirem para entrar.", + onboardingFounder3: "Converse em particular com sua crew.", + onboardingMember1: "Pedir para entrar", + onboardingMember2: "O fundador aprova seu dispositivo.", + onboardingMember3: "Você entrou — as mensagens são criptografadas.", + encryptedNote: "Apenas membros da crew podem ler isto; o coordenador só repassa mensagens criptografadas.", + advanced: "Avançado", + coordinator: "Coordenador", + keyPackage: "Pacote de chaves", + keyPackageReady: "Pronto neste dispositivo", + keyPackageMissing: "Não configurado neste dispositivo", + members: "Membros", + roleFounder: "Fundador", + roleAdmin: "Admin", + roleMember: "Membro", + qrTitle: "Escaneie para entrar no chat privado", + qrHint: "Os membros da crew escaneiam com a câmera do celular. O link abre a aba Chat privado.", + sendFailed: "Não foi possível enviar — toque em Tentar novamente.", + sending: "Enviando…", + sent: "Enviado", + retry: "Tentar novamente", + errorUnreachable: "Não foi possível alcançar o chat privado. Verifique sua conexão.", + errorDetails: "Detalhes", + unreachable: "Inacessível", + dismiss: "Fechar", }, translateUi: { title: "Tradução de mensagens", diff --git a/src/i18n/ro/drift.ts b/src/i18n/ro/drift.ts index a49f4ef..21187df 100644 --- a/src/i18n/ro/drift.ts +++ b/src/i18n/ro/drift.ts @@ -322,7 +322,37 @@ const drift = { "bannerCta": "Configurează", "ready": "Conectat · MLS", "errorPrefix": "Coordonatorul nu a putut fi contactat", - "interactiveHint": "Signerul tău cere aprobare la fiecare acțiune — folosește Reîmprospătează și Trimite după nevoie." + "interactiveHint": "Signerul tău cere aprobare la fiecare acțiune — folosește Reîmprospătează și Trimite după nevoie.", + setupDevice: "Configurează acest dispozitiv", + setupDeviceBody: "Asta îi dă acestui dispozitiv o cheie proprie pentru chatul privat. Un fondator o aprobă o dată.", + waitingApproval: "Se așteaptă aprobarea fondatorului…", + waitingApprovalManual: "Se așteaptă fondatorul. Apasă „Verifică invitația” după ce aprobă.", + onboardingFounder1: "Începe chatul privat.", + onboardingFounder2: "Aprobă membrii care cer să se alăture.", + onboardingFounder3: "Discută în privat cu crew-ul tău.", + onboardingMember1: "Cere să te alături", + onboardingMember2: "Fondatorul îți aprobă dispozitivul.", + onboardingMember3: "Ai intrat — mesajele sunt criptate.", + encryptedNote: "Doar membrii crew-ului pot citi asta; coordonatorul doar retransmite mesaje criptate.", + advanced: "Avansat", + coordinator: "Coordonator", + keyPackage: "Pachet de chei", + keyPackageReady: "Pregătit pe acest dispozitiv", + keyPackageMissing: "Neconfigurat pe acest dispozitiv", + members: "Membri", + roleFounder: "Fondator", + roleAdmin: "Admin", + roleMember: "Membru", + qrTitle: "Scanează pentru a intra în chatul privat", + qrHint: "Membrii crew-ului scanează cu camera telefonului. Linkul deschide fila Chat privat.", + sendFailed: "Trimiterea a eșuat — apasă Reîncearcă.", + sending: "Se trimite…", + sent: "Trimis", + retry: "Reîncearcă", + errorUnreachable: "Chatul privat nu a putut fi contactat. Verifică conexiunea.", + errorDetails: "Detalii", + unreachable: "Inaccesibil", + dismiss: "Închide", }, translateUi: { title: "Traducerea mesajelor", diff --git a/src/i18n/ru/drift.ts b/src/i18n/ru/drift.ts index c668c93..6352ebc 100644 --- a/src/i18n/ru/drift.ts +++ b/src/i18n/ru/drift.ts @@ -327,6 +327,36 @@ const drift = { ready: "Подключено · MLS", errorPrefix: "Не удалось связаться с координатором", interactiveHint: "Ваш подписант запрашивает подтверждение для каждого действия — используйте «Обновить» и «Отправить» по необходимости.", + setupDevice: "Настроить это устройство", + setupDeviceBody: "Устройство получит собственный ключ для приватного чата. Основатель одобрит его один раз.", + waitingApproval: "Ожидание одобрения основателя…", + waitingApprovalManual: "Ожидание основателя. После одобрения нажмите «Проверить приглашение».", + onboardingFounder1: "Запустите приватный чат.", + onboardingFounder2: "Одобряйте участников, которые просят присоединиться.", + onboardingFounder3: "Общайтесь приватно со своим Crew.", + onboardingMember1: "Попросить присоединиться", + onboardingMember2: "Основатель одобрит ваше устройство.", + onboardingMember3: "Вы внутри — сообщения зашифрованы.", + encryptedNote: "Читать могут только участники Crew; координатор лишь передаёт зашифрованные сообщения.", + advanced: "Дополнительно", + coordinator: "Координатор", + keyPackage: "Пакет ключей", + keyPackageReady: "Готово на этом устройстве", + keyPackageMissing: "Не настроено на этом устройстве", + members: "Участники", + roleFounder: "Основатель", + roleAdmin: "Администратор", + roleMember: "Участник", + qrTitle: "Отсканируйте, чтобы войти в приватный чат", + qrHint: "Участники Crew сканируют код камерой телефона. Ссылка открывает вкладку «Приватный чат».", + sendFailed: "Не удалось отправить — нажмите «Повторить».", + sending: "Отправка…", + sent: "Отправлено", + retry: "Повторить", + errorUnreachable: "Не удалось связаться с приватным чатом. Проверьте подключение.", + errorDetails: "Подробности", + unreachable: "Недоступно", + dismiss: "Закрыть", }, translateUi: { title: "Перевод сообщений", diff --git a/src/i18n/sv/drift.ts b/src/i18n/sv/drift.ts index 6ad6cbd..6826b7b 100644 --- a/src/i18n/sv/drift.ts +++ b/src/i18n/sv/drift.ts @@ -327,6 +327,36 @@ const drift = { ready: "Ansluten · MLS", errorPrefix: "Kunde inte nå koordinatorn", interactiveHint: "Din signerare begär godkännande för varje åtgärd — använd Uppdatera och Skicka vid behov.", + setupDevice: "Ställ in den här enheten", + setupDeviceBody: "Det här ger enheten en egen nyckel för den privata chatten. En grundare godkänner den en gång.", + waitingApproval: "Väntar på grundarens godkännande…", + waitingApprovalManual: "Väntar på grundaren. Tryck på ”Kontrollera inbjudan” när de har godkänt.", + onboardingFounder1: "Starta den privata chatten.", + onboardingFounder2: "Godkänn medlemmar som ber att få gå med.", + onboardingFounder3: "Chatta privat med ditt crew.", + onboardingMember1: "Be om att gå med", + onboardingMember2: "Grundaren godkänner din enhet.", + onboardingMember3: "Du är med — meddelandena är krypterade.", + encryptedNote: "Bara crewmedlemmar kan läsa det här; koordinatorn vidarebefordrar bara krypterade meddelanden.", + advanced: "Avancerat", + coordinator: "Koordinator", + keyPackage: "Nyckelpaket", + keyPackageReady: "Redo på den här enheten", + keyPackageMissing: "Inte inställt på den här enheten", + members: "Medlemmar", + roleFounder: "Grundare", + roleAdmin: "Admin", + roleMember: "Medlem", + qrTitle: "Skanna för att gå med i den privata chatten", + qrHint: "Crewmedlemmar skannar det här med telefonkameran. Länken öppnar fliken Privat chatt.", + sendFailed: "Kunde inte skicka — tryck på Försök igen.", + sending: "Skickar…", + sent: "Skickat", + retry: "Försök igen", + errorUnreachable: "Kunde inte nå den privata chatten. Kontrollera din anslutning.", + errorDetails: "Detaljer", + unreachable: "Kan inte nås", + dismiss: "Stäng", }, translateUi: { title: "Meddelandeöversättning", diff --git a/src/i18n/th/drift.ts b/src/i18n/th/drift.ts index e0d895b..fb378f5 100644 --- a/src/i18n/th/drift.ts +++ b/src/i18n/th/drift.ts @@ -319,6 +319,36 @@ const driftStrings = { ready: 'เชื่อมต่อแล้ว · MLS', errorPrefix: 'เชื่อมต่อ coordinator ไม่ได้', interactiveHint: 'ตัวลงนามของคุณขออนุมัติทุกครั้งที่ทำรายการ — ใช้รีเฟรชและส่งตามต้องการ', + setupDevice: "ตั้งค่าอุปกรณ์นี้", + setupDeviceBody: "การทำเช่นนี้จะให้กุญแจเฉพาะแก่อุปกรณ์นี้สำหรับแชทส่วนตัว ผู้ก่อตั้งจะอนุมัติหนึ่งครั้ง", + waitingApproval: "กำลังรอผู้ก่อตั้งอนุมัติ…", + waitingApprovalManual: "กำลังรอผู้ก่อตั้ง แตะ “ตรวจสอบคำเชิญ” หลังจากอนุมัติแล้ว", + onboardingFounder1: "เริ่มแชทส่วนตัว", + onboardingFounder2: "อนุมัติสมาชิกที่ขอเข้าร่วม", + onboardingFounder3: "พูดคุยส่วนตัวกับ crew ของคุณ", + onboardingMember1: 'ขอเข้าร่วม', + onboardingMember2: "ผู้ก่อตั้งจะอนุมัติอุปกรณ์ของคุณ", + onboardingMember3: "คุณเข้าร่วมแล้ว — ข้อความถูกเข้ารหัส", + encryptedNote: "เฉพาะสมาชิก crew เท่านั้นที่อ่านได้ coordinator เพียงส่งต่อข้อความที่เข้ารหัส", + advanced: "ขั้นสูง", + coordinator: "coordinator", + keyPackage: "แพ็กเกจคีย์", + keyPackageReady: "พร้อมบนอุปกรณ์นี้", + keyPackageMissing: "ยังไม่ได้ตั้งค่าบนอุปกรณ์นี้", + members: "สมาชิก", + roleFounder: "ผู้ก่อตั้ง", + roleAdmin: "แอดมิน", + roleMember: "สมาชิก", + qrTitle: "สแกนเพื่อเข้าร่วมแชทส่วนตัว", + qrHint: "สมาชิก crew สแกนด้วยกล้องโทรศัพท์ ลิงก์จะเปิดแท็บแชทส่วนตัว", + sendFailed: "ส่งไม่สำเร็จ — แตะลองใหม่", + sending: "กำลังส่ง…", + sent: "ส่งแล้ว", + retry: "ลองใหม่", + errorUnreachable: "เชื่อมต่อแชทส่วนตัวไม่ได้ ตรวจสอบการเชื่อมต่อของคุณ", + errorDetails: "รายละเอียด", + unreachable: "เชื่อมต่อไม่ได้", + dismiss: "ปิด", }, translateUi: { title: 'การแปลข้อความ', diff --git a/src/i18n/tr/drift.ts b/src/i18n/tr/drift.ts index 3d52031..4a8b990 100644 --- a/src/i18n/tr/drift.ts +++ b/src/i18n/tr/drift.ts @@ -319,6 +319,36 @@ const driftStrings = { ready: 'Bağlandı · MLS', errorPrefix: 'Koordinatöre ulaşılamadı', interactiveHint: "İmzalayıcın her işlem için onay ister — gerektiğinde Yenile ve Gönder'i kullan.", + setupDevice: "Bu cihazı kur", + setupDeviceBody: "Bu, cihaza özel sohbet için kendi anahtarını verir. Bir kurucu bunu bir kez onaylar.", + waitingApproval: "Kurucunun onaylaması bekleniyor…", + waitingApprovalManual: "Kurucu bekleniyor. Onayladıktan sonra “Davetiye kontrol et”e dokun.", + onboardingFounder1: "Özel sohbeti başlat.", + onboardingFounder2: "Katılmak isteyen üyeleri onayla.", + onboardingFounder3: "Crew’inle özel olarak sohbet et.", + onboardingMember1: 'Katılmayı iste', + onboardingMember2: "Kurucu cihazını onaylar.", + onboardingMember3: "İçerdesin — mesajlar şifreli.", + encryptedNote: "Bunu yalnızca crew üyeleri okuyabilir; koordinatör yalnızca şifreli mesajları iletir.", + advanced: "Gelişmiş", + coordinator: "Koordinatör", + keyPackage: "Anahtar paketi", + keyPackageReady: "Bu cihazda hazır", + keyPackageMissing: "Bu cihazda ayarlanmadı", + members: "Üyeler", + roleFounder: "Kurucu", + roleAdmin: "Yönetici", + roleMember: "Üye", + qrTitle: "Özel sohbete katılmak için tara", + qrHint: "Crew üyeleri bunu telefon kamerasıyla tarar. Bağlantı Özel sohbet sekmesini açar.", + sendFailed: "Gönderilemedi — Yeniden dene’ye dokun.", + sending: "Gönderiliyor…", + sent: "Gönderildi", + retry: "Yeniden dene", + errorUnreachable: "Özel sohbete ulaşılamadı. Bağlantını kontrol et.", + errorDetails: "Ayrıntılar", + unreachable: "Ulaşılamıyor", + dismiss: "Kapat", }, translateUi: { title: 'Mesaj çevirisi', diff --git a/src/i18n/uk/drift.ts b/src/i18n/uk/drift.ts index 3105825..78d2b72 100644 --- a/src/i18n/uk/drift.ts +++ b/src/i18n/uk/drift.ts @@ -327,6 +327,36 @@ const drift = { ready: "Підключено · MLS", errorPrefix: "Не вдалося зв'язатися з координатором", interactiveHint: "Ваш підписувач запитує підтвердження для кожної дії — використовуйте «Оновити» та «Надіслати» за потреби.", + setupDevice: "Налаштувати цей пристрій", + setupDeviceBody: "Це дає пристрою власний ключ для приватного чату. Засновник схвалить його один раз.", + waitingApproval: "Очікування схвалення засновника…", + waitingApprovalManual: "Очікування засновника. Після схвалення натисніть «Перевірити запрошення».", + onboardingFounder1: "Запустіть приватний чат.", + onboardingFounder2: "Схвалюйте учасників, які просять приєднатися.", + onboardingFounder3: "Спілкуйтеся приватно зі своєю командою.", + onboardingMember1: "Попросити приєднатися", + onboardingMember2: "Засновник схвалить ваш пристрій.", + onboardingMember3: "Ви всередині — повідомлення зашифровані.", + encryptedNote: "Читати можуть лише учасники команди; координатор лише передає зашифровані повідомлення.", + advanced: "Додатково", + coordinator: "Координатор", + keyPackage: "Пакет ключів", + keyPackageReady: "Готово на цьому пристрої", + keyPackageMissing: "Не налаштовано на цьому пристрої", + members: "Учасники", + roleFounder: "Засновник", + roleAdmin: "Адміністратор", + roleMember: "Учасник", + qrTitle: "Відскануйте, щоб увійти до приватного чату", + qrHint: "Учасники команди сканують код камерою телефона. Посилання відкриває вкладку «Приватний чат».", + sendFailed: "Не вдалося надіслати — натисніть «Повторити».", + sending: "Надсилання…", + sent: "Надіслано", + retry: "Повторити", + errorUnreachable: "Не вдалося зв’язатися з приватним чатом. Перевірте з’єднання.", + errorDetails: "Подробиці", + unreachable: "Недоступно", + dismiss: "Закрити", }, translateUi: { title: "Переклад повідомлень", diff --git a/src/i18n/vi/drift.ts b/src/i18n/vi/drift.ts index 002c8b4..0734000 100644 --- a/src/i18n/vi/drift.ts +++ b/src/i18n/vi/drift.ts @@ -319,6 +319,36 @@ const driftStrings = { ready: 'Đã kết nối · MLS', errorPrefix: 'Không thể kết nối tới điều phối viên', interactiveHint: 'Trình ký của bạn yêu cầu phê duyệt mỗi hành động — dùng Làm mới và Gửi khi cần.', + setupDevice: "Thiết lập thiết bị này", + setupDeviceBody: "Thao tác này cấp cho thiết bị một khóa riêng cho chat riêng tư. Người sáng lập phê duyệt một lần.", + waitingApproval: "Đang chờ người sáng lập phê duyệt…", + waitingApprovalManual: "Đang chờ người sáng lập. Nhấn “Kiểm tra lời mời” sau khi họ phê duyệt.", + onboardingFounder1: "Bắt đầu chat riêng tư.", + onboardingFounder2: "Phê duyệt thành viên xin tham gia.", + onboardingFounder3: "Trò chuyện riêng với crew của bạn.", + onboardingMember1: 'Xin tham gia', + onboardingMember2: "Người sáng lập phê duyệt thiết bị của bạn.", + onboardingMember3: "Bạn đã vào — tin nhắn được mã hóa.", + encryptedNote: "Chỉ thành viên crew mới đọc được; điều phối viên chỉ chuyển tiếp tin nhắn đã mã hóa.", + advanced: "Nâng cao", + coordinator: "Điều phối viên", + keyPackage: "Gói khóa", + keyPackageReady: "Sẵn sàng trên thiết bị này", + keyPackageMissing: "Chưa thiết lập trên thiết bị này", + members: "Thành viên", + roleFounder: "Người sáng lập", + roleAdmin: "Quản trị viên", + roleMember: "Thành viên", + qrTitle: "Quét để tham gia chat riêng tư", + qrHint: "Thành viên crew quét mã bằng camera điện thoại. Liên kết mở tab Chat riêng tư.", + sendFailed: "Không gửi được — nhấn Thử lại.", + sending: "Đang gửi…", + sent: "Đã gửi", + retry: "Thử lại", + errorUnreachable: "Không thể kết nối tới chat riêng tư. Kiểm tra kết nối của bạn.", + errorDetails: "Chi tiết", + unreachable: "Không thể kết nối", + dismiss: "Đóng", }, translateUi: { title: 'Dịch tin nhắn', diff --git a/src/i18n/zh-CN/drift.ts b/src/i18n/zh-CN/drift.ts index d3c5879..c8e7fb3 100644 --- a/src/i18n/zh-CN/drift.ts +++ b/src/i18n/zh-CN/drift.ts @@ -335,6 +335,36 @@ const drift: Dict = { ready: '已连接 · MLS', errorPrefix: '无法连接协调器', interactiveHint: '你的签名器会为每个操作请求批准——按需使用刷新和发送。', + setupDevice: "设置此设备", + setupDeviceBody: "为此设备分配私密聊天专用的密钥。创始人批准一次即可。", + waitingApproval: "正在等待创始人批准…", + waitingApprovalManual: "正在等待创始人。批准后请点击“检查邀请”。", + onboardingFounder1: "开始私密聊天。", + onboardingFounder2: "批准请求加入的成员。", + onboardingFounder3: "与你的 Crew 私密聊天。", + onboardingMember1: '请求加入', + onboardingMember2: "创始人会批准你的设备。", + onboardingMember3: "你已加入——消息已加密。", + encryptedNote: "只有 Crew 成员可以阅读;协调器只转发加密消息。", + advanced: "高级", + coordinator: "协调器", + keyPackage: "密钥包", + keyPackageReady: "已在此设备上就绪", + keyPackageMissing: "尚未在此设备上设置", + members: "成员", + roleFounder: "创始人", + roleAdmin: "管理员", + roleMember: "成员", + qrTitle: "扫描加入私密聊天", + qrHint: "Crew 成员用手机摄像头扫描。链接会打开“私密聊天”标签页。", + sendFailed: "发送失败——点击重试。", + sending: "发送中…", + sent: "已发送", + retry: "重试", + errorUnreachable: "无法连接私密聊天。请检查网络连接。", + errorDetails: "详情", + unreachable: "无法连接", + dismiss: "关闭", }, translateUi: { title: '消息翻译', diff --git a/src/i18n/zh-TW/drift.ts b/src/i18n/zh-TW/drift.ts index 1746bd0..d8de48b 100644 --- a/src/i18n/zh-TW/drift.ts +++ b/src/i18n/zh-TW/drift.ts @@ -335,6 +335,36 @@ const drift: Dict = { ready: '已連線 · MLS', errorPrefix: '無法連線協調器', interactiveHint: '你的簽章器會為每個操作要求核准——視需要使用重新整理和傳送。', + setupDevice: "設定此裝置", + setupDeviceBody: "為此裝置分配私密聊天專用的金鑰。創始人核准一次即可。", + waitingApproval: "正在等待創始人核准…", + waitingApprovalManual: "正在等待創始人。核准後請點「檢查邀請」。", + onboardingFounder1: "開始私密聊天。", + onboardingFounder2: "核准要求加入的成員。", + onboardingFounder3: "與你的 Crew 私密聊天。", + onboardingMember1: '要求加入', + onboardingMember2: "創始人會核准你的裝置。", + onboardingMember3: "你已加入——訊息已加密。", + encryptedNote: "只有 Crew 成員可以閱讀;協調器只轉送加密訊息。", + advanced: "進階", + coordinator: "協調器", + keyPackage: "金鑰包", + keyPackageReady: "已在此裝置上就緒", + keyPackageMissing: "尚未在此裝置上設定", + members: "成員", + roleFounder: "創始人", + roleAdmin: "管理員", + roleMember: "成員", + qrTitle: "掃描加入私密聊天", + qrHint: "Crew 成員用手機相機掃描。連結會開啟「私密聊天」分頁。", + sendFailed: "傳送失敗——點重試。", + sending: "傳送中…", + sent: "已傳送", + retry: "重試", + errorUnreachable: "無法連線私密聊天。請檢查網路連線。", + errorDetails: "詳細資料", + unreachable: "無法連線", + dismiss: "關閉", }, translateUi: { title: '訊息翻譯', diff --git a/src/pages/CrewDetailPage.tsx b/src/pages/CrewDetailPage.tsx index 7c53cc6..f5fb44b 100644 --- a/src/pages/CrewDetailPage.tsx +++ b/src/pages/CrewDetailPage.tsx @@ -42,6 +42,7 @@ import { useTranslation } from '@/contexts/language'; import { useCrew } from '@/hooks/useCrews'; import { useCrewFeed } from '@/hooks/useCrewFeed'; import { useCordnGroupExists } from '@/hooks/useCordnNative'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { genUserName } from '@/lib/genUserName'; @@ -49,7 +50,7 @@ import { SKATE_CREW_KIND, SHOW_CREW_TREASURY, CREW_LOOKING_FOR_LABELS, type Crew import { CREW_LOOKING_FOR_KEYS } from '@/lib/labelKeys'; import { sanitizeUrl, urlDomain } from '@/lib/sanitizeUrl'; import NotFound from './NotFound'; -import { Users, MapPin, ArrowLeft, Calendar, Globe, Instagram, Edit, MessageSquare, Zap, ShieldCheck, Music, Lock } from 'lucide-react'; +import { Users, MapPin, ArrowLeft, Calendar, Globe, Instagram, Edit, MessageSquare, Zap, ShieldCheck, Music, Lock, X } from 'lucide-react'; import type { NostrEvent } from '@nostrify/nostrify'; export default function CrewDetailPage() { @@ -75,6 +76,10 @@ export default function CrewDetailPage() { // already holds the group — used to default the tab and to nudge the founder. const crewGid = crew ? `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}` : ''; const nativeGroupExists = useCordnGroupExists(user?.pubkey, crewGid, activeTab); + const [cordnBannerDismissed, setCordnBannerDismissed] = useLocalStorage( + `skatespots:cordn-banner-dismissed:${pubkey ?? ''}/${identifier ?? ''}`, + false, + ); useEffect(() => { if (nativeGroupExists && !tabTouchedRef.current) setActiveTab('mls'); }, [nativeGroupExists]); @@ -296,29 +301,42 @@ export default function CrewDetailPage() { )} {/* Founder/admin nudge: no private (MLS) chat yet. */} - {canManage && nativeGroupExists === false && activeTab !== 'mls' && ( - - -
- -
-
{t('cordnUi.bannerTitle')}
-
{t('cordnUi.bannerBody')}
-
-
- -
-
- )} + + + +
+ +
+
+ {t('cordnUi.bannerTitle')} +
+
{t('cordnUi.bannerBody')}
+
+
+ +
+
+ )} {/* Activity — one clean tabbed surface instead of three stacked cards */}