fix: harden community joins, approvals and state backups

This commit is contained in:
SkateSpots 2026-10-29 16:29:26 +07:00
parent dcb1a3e674
commit c75ddea8e2
35 changed files with 250 additions and 52 deletions

View file

@ -1535,7 +1535,9 @@ session with **no other memory of this project**. So:
- **Join UX (2026-10-29):** community-specific approval copy names the bot or
admins, an admin pending-request banner links to the room's request panel,
and waiting users get **Request again** plus the second-device hint (use
**Link device**; the bot cannot add a second MLS key for the same account).
**Link device** for a direct handoff, or ask to join again with the new
device; the earlier “already in the group” error was a stale request for the
same key package, not a same-account limitation).
If the bot is missing from the group, the panel warns that an admin must
review requests.
- **Bot:** `scripts/community-bot.ts`, built to CJS with

View file

@ -23,10 +23,10 @@
Cache-Control = "public, max-age=31536000, immutable"
# Human-friendly short links.
# /community and /c -> the app's Community page (HashRouter route).
# /cordn is deliberately the raw Cordn room reference for Staircase/Cordn
# clients; normal users should use /c, which stays inside the app's own native
# MLS flow (Cordn web currently stalls after NIP-42).
# /community, /c and /cordn -> the app's Community page (HashRouter route).
# The raw room reference is still available via "Copy Cordn link" in Advanced
# for Staircase/Cordn clients, but no browser short link goes to the currently
# stalled Cordn web client.
# Keep these BEFORE the SPA fallback.
[[redirects]]
from = "/community"
@ -42,7 +42,7 @@
[[redirects]]
from = "/cordn"
to = "https://cordn.net/chat/cordn1qgdhwumn8ghj7un9d3shjtnnddshgefdwdcx7arn9ehhyecpyqpgz6s9k82wwznal8r39mm0wfe8wsnf6eda3ghjvt8qxvxs3ptfxqq5wd4kzar9wdcx7arn8f3k7mtdw4hxjare8hqt74"
to = "/#/community"
status = 302
force = true

View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Pull the SkateSpots Community Cordn state off the relay host, keep 30 daily
# snapshots locally, refresh the 0600 working copy, and optionally push the
# snapshot directory to restic when the normal relay backup is configured.
#
# scripts/backup-community-state.sh
# SKATESPOTS_COMMUNITY_HOST=skatespots-cvm scripts/backup-community-state.sh
set -euo pipefail
HOST="${SKATESPOTS_COMMUNITY_HOST:-skatespots-cvm}"
REMOTE_STATE="${SKATESPOTS_COMMUNITY_STATE:-/var/lib/skatespots-community/state.json}"
DEST="${SKATESPOTS_COMMUNITY_BACKUP_DIR:-$HOME/.backups/community}"
CONFIG_DIR="$HOME/.config/skatespots"
mkdir -p "$DEST" "$CONFIG_DIR"
chmod 700 "$DEST" "$CONFIG_DIR"
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
scp -q "$HOST:$REMOTE_STATE" "$tmp"
# Refuse to rotate a valid backup out for an empty/corrupt download.
node -e 'const fs=require("fs");const s=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if(!s||typeof s!=="object")process.exit(1)' "$tmp"
stamp="$(date +%F)"
install -m 600 "$tmp" "$DEST/state-$stamp.json"
install -m 600 "$tmp" "$CONFIG_DIR/community-state.json"
echo "saved $DEST/state-$stamp.json"
# Keep 30 daily snapshots.
ls -1t "$DEST"/state-*.json 2>/dev/null | tail -n +31 | xargs -r rm -f
if [[ -f "$CONFIG_DIR/restic.env" ]] && command -v restic >/dev/null 2>&1; then
set -a
# shellcheck disable=SC1091
. "$CONFIG_DIR/restic.env"
set +a
restic backup "$DEST" --tag community-state --host "$(hostname)"
restic forget --tag community-state --keep-daily 30 --prune
else
echo "restic not configured; local snapshots only"
fi

View file

@ -23,6 +23,7 @@
* COMMUNITY_AUTO_APPROVE=0 leave requests for admins instead of adding
* COMMUNITY_MAX_MEMBERS stop auto-approving at this group size (default 250)
* COMMUNITY_MAX_DEVICES_PER_PUBKEY per-account device cap (default 5)
* COMMUNITY_MAX_APPROVALS_PER_HOUR flood brake for auto-approval (default 30)
* COMMUNITY_ONCE=1 one cycle then exit (for a dry run)
*/
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
@ -52,6 +53,10 @@ const parsedMaxDevices = Number(process.env.COMMUNITY_MAX_DEVICES_PER_PUBKEY ??
const MAX_DEVICES_PER_PUBKEY = Number.isFinite(parsedMaxDevices) && parsedMaxDevices >= 1
? Math.floor(parsedMaxDevices)
: 5;
const parsedMaxPerHour = Number(process.env.COMMUNITY_MAX_APPROVALS_PER_HOUR ?? 30);
const MAX_APPROVALS_PER_HOUR = Number.isFinite(parsedMaxPerHour) && parsedMaxPerHour >= 1
? Math.floor(parsedMaxPerHour)
: 30;
const CREATE = process.env.COMMUNITY_CREATE === '1';
const ONCE = process.env.COMMUNITY_ONCE === '1';
@ -139,6 +144,8 @@ let lastJoinRequestAt = 0;
let lastError = '';
let lastPending = 0;
let capLogged = false;
let hourCapLogged = false;
const approvalTimes: number[] = [];
const deviceCapLogged = new Set<string>();
async function cycle(): Promise<void> {
@ -190,6 +197,20 @@ async function cycle(): Promise<void> {
log('member cap cleared; auto-approval resumed');
}
const hourAgo = Date.now() - 60 * 60_000;
while (approvalTimes.length > 0 && (approvalTimes[0] ?? 0) < hourAgo) approvalTimes.shift();
if (approvalTimes.length >= MAX_APPROVALS_PER_HOUR) {
if (!hourCapLogged) {
hourCapLogged = true;
log('hourly approval cap reached', approvalTimes.length, '/', MAX_APPROVALS_PER_HOUR, '— leaving requests for admins');
}
return;
}
if (hourCapLogged) {
hourCapLogged = false;
log('hourly approval cap cleared; auto-approval resumed');
}
const handled: Array<{ pk: string; at: number }> = [];
for (const request of requests.slice(0, 10)) {
if (!/^[0-9a-f]{64}$/i.test(request.pk)) {
@ -230,6 +251,7 @@ async function cycle(): Promise<void> {
}
}
approved.set(approvalKey, Date.now());
approvalTimes.push(Date.now());
handled.push({ pk: request.pk, at: request.at });
log('approved member', short(request.pk));
await sleep(1500); // be gentle with the coordinator
@ -298,6 +320,7 @@ async function main(): Promise<void> {
'auto-approve', AUTO_APPROVE ? 'on' : 'off',
'max', MAX_MEMBERS,
'devices/account', MAX_DEVICES_PER_PUBKEY,
'approvals/hour', MAX_APPROVALS_PER_HOUR,
);
do {

View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Install the daily community-state backup timer for the current user.
set -euo pipefail
cd "$(dirname "$0")/.."
UNIT_DIR="$HOME/.config/systemd/user"
mkdir -p "$UNIT_DIR"
cp scripts/systemd/skatespots-community-backup.service "$UNIT_DIR/"
cp scripts/systemd/skatespots-community-backup.timer "$UNIT_DIR/"
systemctl --user daemon-reload
systemctl --user enable --now skatespots-community-backup.timer
systemctl --user list-timers skatespots-community-backup.timer --no-pager

View file

@ -226,6 +226,12 @@ if command -v ssh >/dev/null 2>&1 && grep -qsE "^Host[[:space:]]+${COMMUNITY_HOS
problems+=("community Cordn bot is ${community_state:-unreachable}")
signature+="community-bot;"
fi
community_backup="$(systemctl --user is-active skatespots-community-backup.timer 2>/dev/null || echo unknown)"
if [ "$community_backup" = "active" ]; then
p " ${G}✓${N} community state backup timer active"
else
p " ${Y}⚠${N} community state backup timer: ${community_backup}"
fi
else
p " ${DIM}ssh alias ${COMMUNITY_HOST} not configured — skipped${N}"
fi

View file

@ -0,0 +1,10 @@
[Unit]
Description=Back up the SkateSpots Community Cordn state
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/home/sovereignsk8/SkateSpots/scripts/backup-community-state.sh
Nice=10
IOSchedulingClass=idle

View file

@ -0,0 +1,10 @@
[Unit]
Description=Daily SkateSpots Community Cordn state backup
[Timer]
OnCalendar=*-*-* 04:17:00
Persistent=true
RandomizedDelaySec=15m
[Install]
WantedBy=timers.target

View file

@ -7,7 +7,6 @@ import {
CheckCheck,
ChevronDown,
Copy,
ExternalLink,
ImagePlus,
KeyRound,
Loader2,
@ -31,6 +30,17 @@ 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 {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
Dialog,
@ -1317,16 +1327,7 @@ function AdvancedPanel({
</Button>
</div>
<div className="flex flex-wrap items-center gap-2">
<a
href={cordnUrl ?? '#'}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-[11px] font-medium text-teal-300 transition-colors hover:text-teal-200"
>
<ExternalLink className="h-3 w-3" />{' '}
{t('socialUi.crewCoordinator.openCordnGroup')}
</a>
<span className="text-[10px] text-gray-600">
<span className="w-full text-[10px] text-gray-600">
{t('cordnUi.cordnWebCaveat')}
</span>
<Button
@ -1340,7 +1341,7 @@ function AdvancedPanel({
) : (
<Copy className="h-3 w-3" />
)}
{copiedCordn ? t('cordnUi.copied') : t('cordnUi.copyRef')}
{copiedCordn ? t('cordnUi.copied') : t('cordnUi.copyCordnLink')}
</Button>
</div>
</>
@ -1486,14 +1487,29 @@ function RequestRow({
return (
<div className="flex items-center justify-between gap-2">
<PersonLabel pubkey={pubkey} botPubkey={botPubkey} botName={botName} />
<Button
size="sm"
disabled={disabled}
onClick={onApprove}
className="h-7 shrink-0 gap-1 text-xs"
>
<UserPlus className="h-3 w-3" /> {t('cordnUi.approve')}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="sm"
disabled={disabled}
className="h-7 shrink-0 gap-1 text-xs"
>
<UserPlus className="h-3 w-3" /> {t('cordnUi.approve')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('cordnUi.approveTitle')}</AlertDialogTitle>
<AlertDialogDescription>
{t('cordnUi.approveDesc', { name: shortNpub(pubkey) })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={onApprove}>{t('cordnUi.approve')}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View file

@ -920,6 +920,7 @@ const batch1 = {
"requestJoin": "Demana d'unir-t'hi",
"requestAgain": "Request again",
"requestHandled": "Request handled",
"copyCordnLink": "Copy Cordn link",
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
"checkInvite": "Comprova si hi ha invitació",
"requested": "Sol·licitud enviada: un administrador de la colla l'aprovarà.",
@ -930,6 +931,8 @@ const batch1 = {
"requests": "Sol·licituds d'accés",
"checkRequests": "Comprova les sol·licituds",
"approve": "Afegeix",
"approveTitle": "Aprovar aquest dispositiu?",
"approveDesc": "Afegir {{name}} a aquesta sala xifrada? Aquest dispositiu podrà llegir els missatges nous.",
"noRequests": "No hi ha sol·licituds pendents.",
"inviteHint": "De moment només hi ets tu en aquest xat: els membres de la colla poden tocar \"Demana d'unir-t'hi\" i després els aproves aquí.",
"shareInvite": "Convida",
@ -1021,7 +1024,7 @@ const batch1 = {
"pendingRequests": "{{count}} sol·licituds d'entrada en espera",
"pendingRequestsDesc": "El bot normalment aprova automàticament. Si s'ha encallat, obre la sala de sota i toca Comprova les sol·licituds.",
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
"secondDeviceHint": "Ja ets membre en un altre dispositiu? Fes servir «Enllaça aquest dispositiu» per a un traspàs directe, o torna a demanar d’entrar amb aquest dispositiu.",
"create": "Crea la sala de la comunitat",
"setupDeviceBody": "Això dóna a aquest dispositiu una clau per a la sala de la comunitat. Un administrador o el bot l'aprova una vegada.",
"onboardingFounder1": "Crea la sala de la comunitat.",

View file

@ -309,6 +309,7 @@ const drift = {
"requestJoin": "Požádat o připojení",
"requestAgain": "Request again",
"requestHandled": "Request handled",
"copyCordnLink": "Copy Cordn link",
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
"checkInvite": "Zkontrolovat pozvánku",
"requested": "Žádost odeslána — administrátor crew ji schválí.",
@ -319,6 +320,8 @@ const drift = {
"requests": "Žádosti o připojení",
"checkRequests": "Zkontrolovat žádosti",
"approve": "Přidat",
"approveTitle": "Schválit toto zařízení?",
"approveDesc": "Přidat {{name}} do této šifrované místnosti? Toto zařízení bude moci číst nové zprávy.",
"noRequests": "Žádné čekající žádosti o připojení.",
"inviteHint": "Zatím jsi v tomto chatu jen ty — členové crew mohou klepnout na „Požádat o připojení“ a ty je zde schválíš.",
"memberCount": "{{count}} ve skupině",
@ -407,7 +410,7 @@ const drift = {
"pendingRequests": "{{count}} čekajících žádostí o připojení",
"pendingRequestsDesc": "Bot obvykle schvaluje automaticky. Pokud se zasekl, otevři níže místnost a klepni na Zkontrolovat žádosti.",
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
"secondDeviceHint": "Už jsi členem na jiném zařízení? Použij „Propojit toto zařízení“ pro přímé předání, nebo požádej o připojení znovu s tímto zařízením.",
"create": "Vytvořit komunitní místnost",
"setupDeviceBody": "Toto zařízení tím získá klíč do komunitní místnosti. Schválí ho jednou administrátor nebo bot.",
"onboardingFounder1": "Vytvoř komunitní místnost.",

View file

@ -311,6 +311,7 @@ const drift = {
requestJoin: "Um Beitritt bitten",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Nach Einladung suchen",
requested: "Anfrage gesendet – ein Crew-Admin wird sie genehmigen.",
@ -321,6 +322,8 @@ const drift = {
requests: "Beitrittsanfragen",
checkRequests: "Anfragen prüfen",
approve: "Hinzufügen",
approveTitle: "Dieses Gerät bestätigen?",
approveDesc: "{{name}} zu diesem verschlüsselten Raum hinzufügen? Dieses Gerät kann dann neue Nachrichten lesen.",
noRequests: "Keine offenen Beitrittsanfragen.",
inviteHint: "Bisher bist nur du in diesem Chat – Crew-Mitglieder können auf „Um Beitritt bitten“ tippen, dann genehmigst du sie hier.",
memberCount: "{{count}} in der Gruppe",
@ -409,7 +412,7 @@ const drift = {
pendingRequests: "{{count}} offene Beitrittsanfragen",
pendingRequestsDesc: "Der Bot genehmigt normalerweise automatisch. Wenn es hängt, öffne unten den Raum und tippe auf Anfragen prüfen.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Schon Mitglied auf einem anderen Gerät? Nutze „Dieses Gerät verknüpfen“ für eine direkte Übergabe oder bitte mit diesem Gerät erneut um Beitritt.",
create: "Community-Raum erstellen",
setupDeviceBody: "Damit erhält dieses Gerät einen Schlüssel für den Community-Raum. Ein Admin oder der Bot bestätigt ihn einmal.",
onboardingFounder1: "Erstelle den Community-Raum.",

View file

@ -313,6 +313,7 @@ const drift = {
requestJoin: "Ζήτα συμμετοχή",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Έλεγχος πρόσκλησης",
requested: "Το αίτημα στάλθηκε — ένας διαχειριστής του crew θα το εγκρίνει.",
@ -323,6 +324,8 @@ const drift = {
requests: "Αιτήματα συμμετοχής",
checkRequests: "Έλεγχος αιτημάτων",
approve: "Προσθήκη",
approveTitle: "Έγκριση αυτής της συσκευής;",
approveDesc: "Προσθήκη του {{name}} σε αυτή την κρυπτογραφημένη αίθουσα; Αυτή η συσκευή θα μπορεί να διαβάζει νέα μηνύματα.",
noRequests: "Δεν υπάρχουν εκκρεμή αιτήματα συμμετοχής.",
inviteHint: "Μόνο εσύ είσαι σε αυτή τη συνομιλία μέχρι στιγμής — τα μέλη του crew μπορούν να πατήσουν «Ζήτα συμμετοχή» και μετά να τα εγκρίνεις εδώ.",
memberCount: "{{count}} στην ομάδα",
@ -411,7 +414,7 @@ const drift = {
pendingRequests: "{{count}} αιτήματα συμμετοχής σε αναμονή",
pendingRequestsDesc: "Το bot εγκρίνει συνήθως αυτόματα. Αν έχει κολλήσει, άνοιξε το δωμάτιο παρακάτω και πάτησε Έλεγχος αιτημάτων.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Είσαι ήδη μέλος σε άλλη συσκευή; Χρησιμοποίησε «Σύνδεση αυτής της συσκευής» για άμεση μεταβίβαση ή ζήτησε ξανά να συμμετάσχεις με αυτή τη συσκευή.",
create: "Δημιουργία δωματίου κοινότητας",
setupDeviceBody: "Αυτό δίνει σε αυτή τη συσκευή ένα κλειδί για το δωμάτιο της κοινότητας. Ένας διαχειριστής ή το bot το εγκρίνει μία φορά.",
onboardingFounder1: "Δημιούργησε το δωμάτιο της κοινότητας.",

View file

@ -920,6 +920,7 @@ const batch1 = {
requestJoin: "Eskatu sartzea",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Egiaztatu gonbidapena",
requested: "Eskaera bidali da — taldeko administratzaile batek onartuko du.",
@ -930,6 +931,8 @@ const batch1 = {
requests: "Sartzeko eskaerak",
checkRequests: "Egiaztatu eskaerak",
approve: "Gehitu",
approveTitle: "Gailu hau onartu?",
approveDesc: "{{name}} gela enkriptatu honetara gehitu? Gailu horrek mezu berriak irakurri ahal izango ditu.",
noRequests: "Ez dago sartzeko eskaera zain.",
inviteHint: "Oraindik zu soilik zaude txat honetan — taldeko kideek \"Eskatu sartzea\" sakatu dezakete, eta gero zuk hemen onartuko dituzu.",
shareInvite: "Gonbidatu",
@ -1021,7 +1024,7 @@ const batch1 = {
pendingRequests: "{{count}} sartzeko eskaera zain",
pendingRequestsDesc: "Botak normalean automatikoki onartzen du. Blokeatuta badago, ireki beheko gela eta sakatu Egiaztatu eskaerak.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Beste gailu batean kide zara jada? Erabili «Estekatu gailu hau» zuzeneko transferentzia egiteko, edo eskatu berriro gailu honekin bat egiteko.",
create: "Sortu komunitate gela",
setupDeviceBody: "Honek gailu honi komunitate gelarako gako bat ematen dio. Administratzaile batek edo botak behin onartzen du.",
onboardingFounder1: "Sortu komunitate gela.",

View file

@ -311,6 +311,7 @@ const drift = {
requestJoin: "Demander à rejoindre",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Vérifier l'invitation",
requested: "Demande envoyée — un admin de la Crew l'approuvera.",
@ -321,6 +322,8 @@ const drift = {
requests: "Demandes d'adhésion",
checkRequests: "Vérifier les demandes",
approve: "Ajouter",
approveTitle: "Approuver cet appareil ?",
approveDesc: "Ajouter {{name}} à ce salon chiffré ? Cet appareil pourra lire les nouveaux messages.",
noRequests: "Aucune demande d'adhésion en attente.",
inviteHint: "Vous êtes seul dans ce chat pour l'instant — les membres de la Crew peuvent appuyer sur « Demander à rejoindre », puis vous les approuvez ici.",
memberCount: "{{count}} dans le groupe",
@ -409,7 +412,7 @@ const drift = {
pendingRequests: "{{count}} demande(s) d’adhésion en attente",
pendingRequestsDesc: "Le bot approuve généralement automatiquement. S’il est bloqué, ouvre le salon ci-dessous et appuie sur Vérifier les demandes.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Déjà membre sur un autre appareil ? Utilise « Lier cet appareil » pour un transfert direct, ou redemande à rejoindre avec cet appareil.",
create: "Créer le salon de la communauté",
setupDeviceBody: "Cela donne à cet appareil une clé pour le salon de la communauté. Un admin ou le bot l’approuve une fois.",
onboardingFounder1: "Crée le salon de la communauté.",

View file

@ -920,6 +920,7 @@ const batch1 = {
requestJoin: "Pedir acceso",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Comprobar se hai invitación",
requested: "Solicitude enviada: un administrador da crew aprobaraa.",
@ -930,6 +931,8 @@ const batch1 = {
requests: "Solicitudes de acceso",
checkRequests: "Comprobar solicitudes",
approve: "Engadir",
approveTitle: "Aprobar este dispositivo?",
approveDesc: "Engadir {{name}} a esta sala cifrada? Ese dispositivo poderá ler as mensaxes novas.",
noRequests: "Non hai solicitudes de acceso pendentes.",
inviteHint: "De momento só estás ti neste chat: os membros da crew poden tocar «Pedir acceso» e despois ti apruébalos aquí.",
shareInvite: "Convidar",
@ -1021,7 +1024,7 @@ const batch1 = {
pendingRequests: "{{count}} solicitudes de entrada en espera",
pendingRequestsDesc: "O bot adoita aprobar automaticamente. Se se atascou, abre a sala de abaixo e toca Comprobar solicitudes.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Xa es membro noutro dispositivo? Usa «Vincular este dispositivo» para un traspaso directo, ou pide entrar de novo con este dispositivo.",
create: "Crear a sala da comunidade",
setupDeviceBody: "Isto dálle a este dispositivo unha chave para a sala da comunidade. Un administrador ou o bot apruxaa unha vez.",
onboardingFounder1: "Crea a sala da comunidade.",

View file

@ -920,6 +920,7 @@ const batch1 = {
"requestJoin": "शामिल होने का अनुरोध करें",
"requestAgain": "Request again",
"requestHandled": "Request handled",
"copyCordnLink": "Copy Cordn link",
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
"checkInvite": "निमंत्रण जाँचें",
"requested": "अनुरोध भेजा गया — क्रू एडमिन इसे स्वीकृत करेगा।",
@ -930,6 +931,8 @@ const batch1 = {
"requests": "शामिल होने के अनुरोध",
"checkRequests": "अनुरोध जाँचें",
"approve": "जोड़ें",
"approveTitle": "इस डिवाइस को स्वीकृत करें?",
"approveDesc": "{{name}} को इस एन्क्रिप्टेड रूम में जोड़ें? वह डिवाइस नए संदेश पढ़ सकेगा।",
"noRequests": "कोई लंबित शामिल होने का अनुरोध नहीं।",
"inviteHint": "अभी तक इस चैट में केवल आप हैं — क्रू सदस्य “शामिल होने का अनुरोध करें” टैप कर सकते हैं, फिर आप उन्हें यहाँ स्वीकृत करें।",
"shareInvite": "निमंत्रण दें",
@ -1021,7 +1024,7 @@ const batch1 = {
"pendingRequests": "{{count}} शामिल होने के अनुरोध इंतज़ार में",
"pendingRequestsDesc": "बॉट आमतौर पर अपने आप मंज़ूरी देता है। अगर अटका हुआ है, तो नीचे रूम खोलें और “अनुरोध जाँचें” टैप करें।",
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
"secondDeviceHint": "क्या आप पहले से किसी दूसरे डिवाइस पर सदस्य हैं? सीधे हैंडऑफ़ के लिए “इस डिवाइस को लिंक करें” का उपयोग करें, या इस डिवाइस से दोबारा जुड़ने का अनुरोध करें।",
"create": "कम्युनिटी रूम बनाएँ",
"setupDeviceBody": "इससे इस डिवाइस को कम्युनिटी रूम की एक कुंजी मिलती है। कोई एडमिन या बॉट इसे एक बार मंज़ूर करता है।",
"onboardingFounder1": "कम्युनिटी रूम बनाएँ।",

View file

@ -305,6 +305,7 @@ const driftStrings = {
requestJoin: 'Minta gabung',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: 'Periksa undangan',
requested: 'Permintaan terkirim — admin crew akan menyetujuinya.',
@ -315,6 +316,8 @@ const driftStrings = {
requests: 'Permintaan gabung',
checkRequests: 'Periksa permintaan',
approve: 'Tambah',
approveTitle: "Setujui perangkat ini?",
approveDesc: "Tambahkan {{name}} ke ruang terenkripsi ini? Perangkat itu akan dapat membaca pesan baru.",
noRequests: 'Tidak ada permintaan gabung yang tertunda.',
inviteHint: 'Baru kamu yang ada di chat ini — anggota crew bisa menekan “Minta gabung”, lalu kamu setujui di sini.',
memberCount: '{{count}} di grup',
@ -403,7 +406,7 @@ const driftStrings = {
pendingRequests: "{{count}} permintaan bergabung menunggu",
pendingRequestsDesc: "Bot biasanya menyetujui secara otomatis. Jika macet, buka ruang di bawah dan ketuk Periksa permintaan.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Sudah menjadi anggota di perangkat lain? Gunakan “Tautkan perangkat ini” untuk serah terima langsung, atau minta bergabung lagi dengan perangkat ini.",
create: "Buat ruang komunitas",
setupDeviceBody: "Ini memberi perangkat ini kunci untuk ruang komunitas. Admin atau bot menyetujuinya satu kali.",
onboardingFounder1: "Buat ruang komunitas.",

View file

@ -311,6 +311,7 @@ const drift = {
requestJoin: "Chiedi di unirti",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Controlla l'invito",
requested: "Richiesta inviata — un admin della crew la approverà.",
@ -321,6 +322,8 @@ const drift = {
requests: "Richieste di adesione",
checkRequests: "Controlla le richieste",
approve: "Aggiungi",
approveTitle: "Approva questo dispositivo?",
approveDesc: "Aggiungere {{name}} a questa stanza cifrata? Quel dispositivo potrà leggere i nuovi messaggi.",
noRequests: "Nessuna richiesta di adesione in sospeso.",
inviteHint: "Per ora sei solo tu in questa chat — i membri della crew possono toccare “Chiedi di unirti”, poi li approvi qui.",
memberCount: "{{count}} nel gruppo",
@ -409,7 +412,7 @@ const drift = {
pendingRequests: "{{count}} richieste di adesione in attesa",
pendingRequestsDesc: "Il bot di solito approva automaticamente. Se è bloccato, apri la stanza qui sotto e tocca Controlla le richieste.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Sei già membro su un altro dispositivo? Usa «Collega questo dispositivo» per un passaggio diretto, oppure chiedi di nuovo di entrare con questo dispositivo.",
create: "Crea la stanza della community",
setupDeviceBody: "Questo assegna a questo dispositivo una chiave per la stanza della community. Un admin o il bot la approva una volta.",
onboardingFounder1: "Crea la stanza della community.",

View file

@ -321,6 +321,7 @@ const drift: Dict = {
requestJoin: '参加をリクエスト',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: '招待を確認',
requested: 'リクエストを送信しました — クルー管理者が承認します。',
@ -331,6 +332,8 @@ const drift: Dict = {
requests: '参加リクエスト',
checkRequests: 'リクエストを確認',
approve: '追加',
approveTitle: "このデバイスを承認しますか?",
approveDesc: "{{name}} をこの暗号化ルームに追加しますか?そのデバイスは新しいメッセージを読めるようになります。",
noRequests: '保留中の参加リクエストはありません。',
inviteHint: '今のところこのチャットにはあなただけです — クルーメンバーは「参加をリクエスト」をタップでき、あなたはここで承認します。',
memberCount: 'グループ内 {{count}}人',
@ -419,7 +422,7 @@ const drift: Dict = {
pendingRequests: "参加リクエスト {{count}} 件が待機中",
pendingRequestsDesc: "ボットは通常自動で承認します。止まっている場合は、下のルームを開いて「リクエストを確認」をタップしてください。",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "別のデバイスで既にメンバーですか?直接引き継ぐには「このデバイスをリンク」を使うか、このデバイスで改めて参加をリクエストしてください。",
create: "コミュニティルームを作成",
setupDeviceBody: "このデバイスにコミュニティルームの鍵を付与します。管理者またはボットが一度だけ承認します。",
onboardingFounder1: "コミュニティルームを作成します。",

View file

@ -321,6 +321,7 @@ const drift: Dict = {
requestJoin: '참여 요청',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: '초대 확인',
requested: '요청을 보냈습니다 — 크루 관리자가 승인합니다.',
@ -331,6 +332,8 @@ const drift: Dict = {
requests: '참여 요청',
checkRequests: '요청 확인',
approve: '추가',
approveTitle: "이 기기를 승인할까요?",
approveDesc: "{{name}} 님을 이 암호화된 방에 추가할까요? 해당 기기는 새 메시지를 읽을 수 있게 됩니다.",
noRequests: '대기 중인 참여 요청이 없습니다.',
inviteHint: '지금까지 이 채팅에는 당신만 있습니다 — 크루 멤버가 “참여 요청”을 누르면 여기서 승인할 수 있습니다.',
memberCount: '그룹에 {{count}}명',
@ -419,7 +422,7 @@ const drift: Dict = {
pendingRequests: "참여 요청 {{count}}개 대기 중",
pendingRequestsDesc: "봇은 보통 자동으로 승인합니다. 멈춰 있으면 아래 방을 열고 요청 확인을 탭하세요.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "다른 기기에서 이미 멤버인가요? 직접 인계하려면 “이 기기 연결”을 사용하거나, 이 기기로 다시 참여를 요청하세요.",
create: "커뮤니티 방 만들기",
setupDeviceBody: "이 기기에 커뮤니티 방 열쇠를 부여합니다. 관리자나 봇이 한 번 승인합니다.",
onboardingFounder1: "커뮤니티 방을 만드세요.",

View file

@ -21,7 +21,7 @@ const communityUi = {
pendingRequests: '{{count}} join request(s) waiting',
pendingRequestsDesc: 'The bot usually approves automatically. If it’s stuck, open the room below and tap Check requests.',
botMissing: 'The community bot isn’t in this room right now, so an admin will review join requests.',
secondDeviceHint: 'Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.',
secondDeviceHint: 'Already a member on another device? Use “Link device” for a direct handoff, or ask to join again with this device.',
create: 'Create the community room',
setupDeviceBody:
'This gives this device a key for the community room. An admin or the bot approves it once.',
@ -61,7 +61,7 @@ const communityUi = {
pendingRequests: '{{count}} solicitud(es) de entrada esperando',
pendingRequestsDesc: 'El bot suele aprobar automáticamente. Si se ha quedado atascado, abre la sala y pulsa Buscar solicitudes.',
botMissing: 'El bot de la comunidad no está en esta sala ahora mismo; un administrador revisará las solicitudes.',
secondDeviceHint: '¿Ya eres miembro en otro dispositivo? Usa «Vincular dispositivo»; el bot no puede añadir una segunda clave a la misma cuenta.',
secondDeviceHint: '¿Ya eres miembro en otro dispositivo? Usa «Vincular dispositivo» para un traspaso directo o pide entrar de nuevo con este dispositivo.',
create: 'Crear la sala de la comunidad',
setupDeviceBody:
'Esto da a este dispositivo una clave para la sala de la comunidad. Un administrador o el bot la aprueba una vez.',

View file

@ -18,6 +18,7 @@ const cordnUi = {
requestJoin: 'Ask to join',
requestAgain: 'Request again',
requestHandled: 'Request handled',
copyCordnLink: 'Copy Cordn link',
cordnWebCaveat: 'Cordn web may stall after sign-in; the in-app join is more reliable.',
checkInvite: 'Check for invite',
requested: 'Request sent — a crew admin will approve it.',
@ -28,6 +29,8 @@ const cordnUi = {
requests: 'Join requests',
checkRequests: 'Check requests',
approve: 'Add',
approveTitle: 'Approve this device?',
approveDesc: 'Add {{name}} to this encrypted room? That device will be able to read new messages.',
noRequests: 'No pending join requests.',
inviteHint: 'Only you are in this chat so far — crew members can tap “Ask to join”, then you approve them here.',
shareInvite: 'Invite',
@ -123,6 +126,7 @@ const cordnUi = {
requestJoin: 'Pedir entrar',
requestAgain: 'Volver a pedir',
requestHandled: 'Solicitud gestionada',
copyCordnLink: 'Copiar enlace de Cordn',
cordnWebCaveat: 'Cordn web puede quedarse colgado tras iniciar sesión; entrar desde la app es más fiable.',
checkInvite: 'Buscar invitación',
requested: 'Solicitud enviada: un administrador la aprobará.',
@ -133,6 +137,8 @@ const cordnUi = {
requests: 'Solicitudes de entrada',
checkRequests: 'Ver solicitudes',
approve: 'Añadir',
approveTitle: '¿Aprobar este dispositivo?',
approveDesc: '¿Añadir a {{name}} a esta sala cifrada? Ese dispositivo podrá leer los mensajes nuevos.',
noRequests: 'No hay solicitudes pendientes.',
inviteHint: 'De momento solo estás tú en este chat: los miembros pueden pulsar «Pedir entrar» y tú los apruebas aquí.',
shareInvite: 'Invitar',

View file

@ -309,6 +309,7 @@ const drift = {
"requestJoin": "Vragen om deel te nemen",
"requestAgain": "Request again",
"requestHandled": "Request handled",
"copyCordnLink": "Copy Cordn link",
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
"checkInvite": "Op uitnodiging controleren",
"requested": "Verzoek verzonden — een crew-admin keurt het goed.",
@ -319,6 +320,8 @@ const drift = {
"requests": "Deelnameverzoeken",
"checkRequests": "Verzoeken controleren",
"approve": "Toevoegen",
"approveTitle": "Dit apparaat goedkeuren?",
"approveDesc": "{{name}} aan deze versleutelde ruimte toevoegen? Dat apparaat kan dan nieuwe berichten lezen.",
"noRequests": "Geen openstaande deelnameverzoeken.",
"inviteHint": "Alleen jij zit tot nu toe in deze chat — crewleden kunnen op “Vragen om deel te nemen” tikken, daarna keur je hen hier goed.",
"memberCount": "{{count}} in de groep",
@ -407,7 +410,7 @@ const drift = {
"pendingRequests": "{{count}} openstaande verzoeken om deel te nemen",
"pendingRequestsDesc": "De bot keurt meestal automatisch goed. Als het vastloopt, open dan hieronder de ruimte en tik op Verzoeken controleren.",
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
"secondDeviceHint": "Al lid op een ander apparaat? Gebruik ‘Dit apparaat koppelen’ voor een directe overdracht, of vraag opnieuw om lid te worden met dit apparaat.",
"create": "Communityruimte aanmaken",
"setupDeviceBody": "Dit geeft dit apparaat een sleutel voor de communityruimte. Een beheerder of de bot keurt die één keer goed.",
"onboardingFounder1": "Maak de communityruimte aan.",

View file

@ -309,6 +309,7 @@ const drift = {
"requestJoin": "Poproś o dołączenie",
"requestAgain": "Request again",
"requestHandled": "Request handled",
"copyCordnLink": "Copy Cordn link",
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
"checkInvite": "Sprawdź zaproszenie",
"requested": "Prośba wysłana — administrator crew ją zatwierdzi.",
@ -319,6 +320,8 @@ const drift = {
"requests": "Prośby o dołączenie",
"checkRequests": "Sprawdź prośby",
"approve": "Dodaj",
"approveTitle": "Zatwierdzić to urządzenie?",
"approveDesc": "Dodać {{name}} do tego zaszyfrowanego pokoju? To urządzenie będzie mogło czytać nowe wiadomości.",
"noRequests": "Brak oczekujących próśb o dołączenie.",
"inviteHint": "Na razie tylko ty jesteś w tym czacie — członkowie crew mogą dotknąć „Poproś o dołączenie”, a ty ich tu zatwierdzisz.",
"memberCount": "{{count}} w grupie",
@ -407,7 +410,7 @@ const drift = {
"pendingRequests": "{{count}} oczekujących próśb o dołączenie",
"pendingRequestsDesc": "Bot zwykle zatwierdza automatycznie. Jeśli utknął, otwórz pokój poniżej i dotknij Sprawdź prośby.",
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
"secondDeviceHint": "Masz już konto na innym urządzeniu? Użyj „Połącz to urządzenie”, aby przenieść je bezpośrednio, albo poproś ponownie o dołączenie z tego urządzenia.",
"create": "Utwórz pokój społeczności",
"setupDeviceBody": "To daje temu urządzeniu klucz do pokoju społeczności. Administrator lub bot zatwierdza go jednorazowo.",
"onboardingFounder1": "Utwórz pokój społeczności.",

View file

@ -311,6 +311,7 @@ const drift = {
requestJoin: "Pedir para entrar",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Verificar convite",
requested: "Solicitação enviada — um admin da crew vai aprovar.",
@ -321,6 +322,8 @@ const drift = {
requests: "Pedidos de entrada",
checkRequests: "Verificar pedidos",
approve: "Adicionar",
approveTitle: "Aprovar este dispositivo?",
approveDesc: "Adicionar {{name}} a esta sala criptografada? Esse dispositivo poderá ler as novas mensagens.",
noRequests: "Nenhum pedido de entrada pendente.",
inviteHint: "Por enquanto só você está neste chat — os membros da crew podem tocar em “Pedir para entrar”, e você os aprova aqui.",
memberCount: "{{count}} no grupo",
@ -409,7 +412,7 @@ const drift = {
pendingRequests: "{{count}} pedido(s) de entrada aguardando",
pendingRequestsDesc: "O bot costuma aprovar automaticamente. Se travou, abra a sala abaixo e toque em Verificar pedidos.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Já é membro em outro dispositivo? Use “Vincular este dispositivo” para uma transferência direta, ou peça para entrar de novo com este dispositivo.",
create: "Criar a sala da comunidade",
setupDeviceBody: "Isso dá a este dispositivo uma chave para a sala da comunidade. Um admin ou o bot aprova uma vez.",
onboardingFounder1: "Crie a sala da comunidade.",

View file

@ -309,6 +309,7 @@ const drift = {
"requestJoin": "Cere să te alături",
"requestAgain": "Request again",
"requestHandled": "Request handled",
"copyCordnLink": "Copy Cordn link",
"cordnWebCaveat": "Cordn web may stall after sign-in; the in-app join is more reliable.",
"checkInvite": "Verifică invitația",
"requested": "Cerere trimisă — un admin de crew o va aproba.",
@ -319,6 +320,8 @@ const drift = {
"requests": "Cereri de alăturare",
"checkRequests": "Verifică cererile",
"approve": "Adaugă",
"approveTitle": "Aprobi acest dispozitiv?",
"approveDesc": "Adaugi {{name}} în această cameră criptată? Dispozitivul respectiv va putea citi mesajele noi.",
"noRequests": "Nicio cerere de alăturare în așteptare.",
"inviteHint": "Momentan doar tu ești în acest chat — membrii crew-ului pot apăsa „Cere să te alături”, apoi îi aprobi aici.",
"memberCount": "{{count}} în grup",
@ -407,7 +410,7 @@ const drift = {
"pendingRequests": "{{count}} cereri de alăturare în așteptare",
"pendingRequestsDesc": "Botul aprobă de obicei automat. Dacă s-a blocat, deschide camera de mai jos și apasă Verifică cererile.",
"botMissing": "The community bot isn’t in this room right now, so an admin will review join requests.",
"secondDeviceHint": "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
"secondDeviceHint": "Ești deja membru pe alt dispozitiv? Folosește „Conectează acest dispozitiv” pentru un transfer direct sau cere din nou să te alături cu acest dispozitiv.",
"create": "Creează camera comunității",
"setupDeviceBody": "Aceasta oferă acestui dispozitiv o cheie pentru camera comunității. Un administrator sau botul o aprobă o dată.",
"onboardingFounder1": "Creează camera comunității.",

View file

@ -313,6 +313,7 @@ const drift = {
requestJoin: "Попросить присоединиться",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Проверить приглашение",
requested: "Запрос отправлен — администратор Crew одобрит его.",
@ -323,6 +324,8 @@ const drift = {
requests: "Запросы на присоединение",
checkRequests: "Проверить запросы",
approve: "Добавить",
approveTitle: "Одобрить это устройство?",
approveDesc: "Добавить {{name}} в эту зашифрованную комнату? Это устройство сможет читать новые сообщения.",
noRequests: "Нет ожидающих запросов на присоединение.",
inviteHint: "Пока в этом чате только вы — участники Crew могут нажать «Попросить присоединиться», а вы одобрите их здесь.",
memberCount: "{{count}} в группе",
@ -411,7 +414,7 @@ const drift = {
pendingRequests: "{{count}} запросов на вступление в ожидании",
pendingRequestsDesc: "Бот обычно одобряет автоматически. Если застряло, открой комнату ниже и нажми «Проверить запросы».",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Уже участник на другом устройстве? Используй «Привязать это устройство» для прямой передачи или попроси снова присоединиться с этого устройства.",
create: "Создать комнату сообщества",
setupDeviceBody: "Это даёт устройству ключ от комнаты сообщества. Администратор или бот подтверждает его один раз.",
onboardingFounder1: "Создай комнату сообщества.",

View file

@ -313,6 +313,7 @@ const drift = {
requestJoin: "Be om att gå med",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Kontrollera inbjudan",
requested: "Begäran skickad — en crewadmin godkänner den.",
@ -323,6 +324,8 @@ const drift = {
requests: "Medlemsbegäranden",
checkRequests: "Kontrollera begäranden",
approve: "Lägg till",
approveTitle: "Godkänn den här enheten?",
approveDesc: "Lägg till {{name}} i den här krypterade rummet? Enheten kommer att kunna läsa nya meddelanden.",
noRequests: "Inga väntande medlemsbegäranden.",
inviteHint: "Bara du är i den här chatten än så länge — crewmedlemmar kan trycka på ”Be om att gå med”, sedan godkänner du dem här.",
memberCount: "{{count}} i gruppen",
@ -411,7 +414,7 @@ const drift = {
pendingRequests: "{{count}} ansökningar om att gå med väntar",
pendingRequestsDesc: "Boten godkänner oftast automatiskt. Om det har fastnat, öppna rummet nedan och tryck på Kontrollera begäranden.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Redan medlem på en annan enhet? Använd ”Länka den här enheten” för en direkt överlämning, eller be att få gå med igen med den här enheten.",
create: "Skapa communityrummet",
setupDeviceBody: "Detta ger enheten en nyckel till communityrummet. En admin eller boten godkänner den en gång.",
onboardingFounder1: "Skapa communityrummet.",

View file

@ -305,6 +305,7 @@ const driftStrings = {
requestJoin: 'ขอเข้าร่วม',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: 'ตรวจสอบคำเชิญ',
requested: 'ส่งคำขอแล้ว — แอดมิน crew จะอนุมัติ',
@ -315,6 +316,8 @@ const driftStrings = {
requests: 'คำขอเข้าร่วม',
checkRequests: 'ตรวจสอบคำขอ',
approve: 'เพิ่ม',
approveTitle: "อนุมัติอุปกรณ์นี้หรือไม่",
approveDesc: "เพิ่ม {{name}} เข้าห้องเข้ารหัสนี้หรือไม่ อุปกรณ์นั้นจะอ่านข้อความใหม่ได้",
noRequests: 'ไม่มีคำขอเข้าร่วมที่ค้างอยู่',
inviteHint: 'ในแชทนี้มีแค่คุณเท่านั้น — สมาชิก crew สามารถแตะ “ขอเข้าร่วม” แล้วคุณอนุมัติที่นี่',
memberCount: '{{count}} คนในกลุ่ม',
@ -403,7 +406,7 @@ const driftStrings = {
pendingRequests: "มีคำขอเข้าร่วม {{count}} รายการรออยู่",
pendingRequestsDesc: "บอทมักอนุมัติโดยอัตโนมัติ หากค้างอยู่ ให้เปิดห้องด้านล่างแล้วแตะ ตรวจสอบคำขอ",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "เป็นสมาชิกบนอุปกรณ์อื่นอยู่แล้วหรือ? ใช้ “เชื่อมโยงอุปกรณ์นี้” เพื่อโอนย้ายโดยตรง หรือขอเข้าร่วมอีกครั้งด้วยอุปกรณ์นี้",
create: "สร้างห้องชุมชน",
setupDeviceBody: "สิ่งนี้มอบกุญแจสำหรับห้องชุมชนให้อุปกรณ์นี้ ผู้ดูแลหรือบอทจะอนุมัติหนึ่งครั้ง",
onboardingFounder1: "สร้างห้องชุมชน",

View file

@ -305,6 +305,7 @@ const driftStrings = {
requestJoin: 'Katılmayı iste',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: 'Davetiye kontrol et',
requested: 'İstek gönderildi — bir crew yöneticisi onaylayacak.',
@ -315,6 +316,8 @@ const driftStrings = {
requests: 'Katılma istekleri',
checkRequests: 'İstekleri kontrol et',
approve: 'Ekle',
approveTitle: "Bu cihaz onaylansın mı?",
approveDesc: "{{name}} bu şifreli odaya eklensin mi? Bu cihaz yeni mesajları okuyabilecek.",
noRequests: 'Bekleyen katılma isteği yok.',
inviteHint: "Bu sohbette şu ana kadar yalnızca sen varsın — crew üyeleri “Katılmayı iste”ye dokunabilir, sonra burada onaylarsın.",
memberCount: 'grupta {{count}} kişi',
@ -403,7 +406,7 @@ const driftStrings = {
pendingRequests: "{{count}} katılma isteği bekliyor",
pendingRequestsDesc: "Bot genellikle otomatik onaylar. Takıldıysa aşağıdaki odayı aç ve İstekleri kontrol et’e dokun.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Başka bir cihazda zaten üye misiniz? Doğrudan devir için “Bu cihazı bağla”yı kullanın veya bu cihazla yeniden katılma isteği gönderin.",
create: "Topluluk odasını oluştur",
setupDeviceBody: "Bu, bu cihaza topluluk odası için bir anahtar verir. Bir yönetici veya bot bunu bir kez onaylar.",
onboardingFounder1: "Topluluk odasını oluştur.",

View file

@ -313,6 +313,7 @@ const drift = {
requestJoin: "Попросити приєднатися",
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: "Перевірити запрошення",
requested: "Запит надіслано — адміністратор команди його схвалить.",
@ -323,6 +324,8 @@ const drift = {
requests: "Запити на приєднання",
checkRequests: "Перевірити запити",
approve: "Додати",
approveTitle: "Схвалити цей пристрій?",
approveDesc: "Додати {{name}} до цієї зашифрованої кімнати? Цей пристрій зможе читати нові повідомлення.",
noRequests: "Немає запитів на приєднання, що очікують.",
inviteHint: "Поки в цьому чаті лише ви — учасники команди можуть натиснути «Попросити приєднатися», а ви схвалите їх тут.",
memberCount: "{{count}} у групі",
@ -411,7 +414,7 @@ const drift = {
pendingRequests: "{{count}} запитів на приєднання в очікуванні",
pendingRequestsDesc: "Бот зазвичай схвалює автоматично. Якщо застрягло, відкрий кімнату нижче й натисни «Перевірити запити».",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Уже є учасником на іншому пристрої? Використай «Прив’язати цей пристрій» для прямого перенесення або попроси знову долучитися з цього пристрою.",
create: "Створити кімнату спільноти",
setupDeviceBody: "Це дає цьому пристрою ключ до кімнати спільноти. Адміністратор або бот підтверджує його один раз.",
onboardingFounder1: "Створи кімнату спільноти.",

View file

@ -305,6 +305,7 @@ const driftStrings = {
requestJoin: 'Xin tham gia',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: 'Kiểm tra lời mời',
requested: 'Đã gửi yêu cầu — quản trị viên crew sẽ duyệt.',
@ -315,6 +316,8 @@ const driftStrings = {
requests: 'Yêu cầu tham gia',
checkRequests: 'Kiểm tra yêu cầu',
approve: 'Thêm',
approveTitle: "Phê duyệt thiết bị này?",
approveDesc: "Thêm {{name}} vào phòng được mã hóa này? Thiết bị đó sẽ có thể đọc các tin nhắn mới.",
noRequests: 'Không có yêu cầu tham gia đang chờ.',
inviteHint: 'Mới chỉ có bạn trong chat này — thành viên crew có thể nhấn “Xin tham gia”, rồi bạn duyệt họ ở đây.',
memberCount: '{{count}} trong nhóm',
@ -403,7 +406,7 @@ const driftStrings = {
pendingRequests: "{{count}} yêu cầu tham gia đang chờ",
pendingRequestsDesc: "Bot thường phê duyệt tự động. Nếu bị kẹt, mở phòng bên dưới và nhấn Kiểm tra yêu cầu.",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "Đã là thành viên trên thiết bị khác? Dùng “Liên kết thiết bị này” để chuyển giao trực tiếp, hoặc yêu cầu tham gia lại bằng thiết bị này.",
create: "Tạo phòng cộng đồng",
setupDeviceBody: "Việc này cấp cho thiết bị này một khóa cho phòng cộng đồng. Quản trị viên hoặc bot phê duyệt một lần.",
onboardingFounder1: "Tạo phòng cộng đồng.",

View file

@ -321,6 +321,7 @@ const drift: Dict = {
requestJoin: '请求加入',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: '检查邀请',
requested: '请求已发送——Crew 管理员会批准。',
@ -331,6 +332,8 @@ const drift: Dict = {
requests: '加入请求',
checkRequests: '检查请求',
approve: '添加',
approveTitle: "批准此设备?",
approveDesc: "将 {{name}} 添加到此加密房间?该设备将能读取新消息。",
noRequests: '没有待处理的加入请求。',
inviteHint: '目前只有你在这个聊天中——Crew 成员可以点击“请求加入”,然后你在这里批准他们。',
memberCount: '群组中有 {{count}} 人',
@ -419,7 +422,7 @@ const drift: Dict = {
pendingRequests: "{{count}} 个加入请求正在等待",
pendingRequestsDesc: "机器人通常会自动批准。如果卡住了,请打开下面的房间并点按“检查请求”。",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "已在其他设备上加入?使用“关联此设备”直接交接,或用此设备再次申请加入。",
create: "创建社区房间",
setupDeviceBody: "这会为此设备提供社区房间的密钥,管理员或机器人会批准一次。",
onboardingFounder1: "创建社区房间。",

View file

@ -321,6 +321,7 @@ const drift: Dict = {
requestJoin: '要求加入',
requestAgain: "Request again",
requestHandled: "Request handled",
copyCordnLink: "Copy Cordn link",
cordnWebCaveat: "Cordn web may stall after sign-in; the in-app join is more reliable.",
checkInvite: '檢查邀請',
requested: '要求已送出——Crew 管理員會批准。',
@ -331,6 +332,8 @@ const drift: Dict = {
requests: '加入要求',
checkRequests: '檢查要求',
approve: '新增',
approveTitle: "批准此裝置?",
approveDesc: "將 {{name}} 加入此加密房間?該裝置將能讀取新訊息。",
noRequests: '沒有待處理的加入要求。',
inviteHint: '目前只有你在這個聊天中——Crew 成員可以點「要求加入」,然後你在這裡批准他們。',
memberCount: '群組中有 {{count}} 人',
@ -419,7 +422,7 @@ const drift: Dict = {
pendingRequests: "{{count}} 個加入要求正在等待",
pendingRequestsDesc: "機器人通常會自動核准。如果卡住了,請開啟下面的房間並點按「檢查要求」。",
botMissing: "The community bot isn’t in this room right now, so an admin will review join requests.",
secondDeviceHint: "Already a member on another device? Use “Link device” instead — the bot can’t add a second key to the same account.",
secondDeviceHint: "已在其他裝置上加入?使用「連結此裝置」直接交接,或用此裝置再次申請加入。",
create: "建立社群房間",
setupDeviceBody: "這會為此裝置提供社群房間的金鑰,管理員或機器人會核准一次。",
onboardingFounder1: "建立社群房間。",