Add QR scanner to Send dialog

The recipient input on /wallet's Send dialog now has a camera button
that opens a QR scanner. Bitcoin BIP-21 URIs are parsed and the
silent-payment fallback (?sp=) is preferred when present, falling
back to the on-chain address otherwise. Plain addresses, sp1… codes,
npub, and nprofile values are dropped into the input verbatim and
resolved by the existing recipient logic.

QrScannerDialog is a standalone component (ported from Ditto) that
owns the camera lifecycle via getUserMedia and the qr-scanner npm
package. It surfaces failure modes (insecure context, denied
permission, no camera, busy camera, overconstrained, ready timeout)
instead of a silent black screen, and offers a flash toggle when the
device supports it.

Android needed an explicit CAMERA permission in the manifest; iOS's
existing NSCameraUsageDescription string was extended to mention QR
scanning. No Capacitor camera plugin is required — the standard web
APIs work inside WKWebView and Android's WebView.
This commit is contained in:
Alex Gleason
2026-05-28 06:02:58 -05:00
parent 843fb29f26
commit bae49e6123
24 changed files with 764 additions and 21 deletions
+2
View File
@@ -60,4 +60,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>
+1 -1
View File
@@ -50,7 +50,7 @@
<key>NSPhotoLibraryUsageDescription</key>
<string>Agora needs access to your photo library to upload images to your posts and profile.</string>
<key>NSCameraUsageDescription</key>
<string>Agora needs camera access to take photos and videos for your posts.</string>
<string>Agora needs camera access to take photos and videos for your posts, and to scan QR codes when sending Bitcoin.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Agora needs access to your microphone to record voice messages.</string>
<key>ITSAppUsesNonExemptEncryption</key>
+16
View File
@@ -105,6 +105,7 @@
"iso-3166": "^4.4.0",
"lucide-react": "^1.8.0",
"nostr-tools": "^2.13.0",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"react": "^19.2.4",
"react-blurhash": "^0.3.0",
@@ -6226,6 +6227,12 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
"license": "MIT"
},
"node_modules/@types/qrcode": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
@@ -11997,6 +12004,15 @@
"node": ">=6"
}
},
"node_modules/qr-scanner": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/qr-scanner/-/qr-scanner-1.4.2.tgz",
"integrity": "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==",
"license": "MIT",
"dependencies": {
"@types/offscreencanvas": "^2019.6.4"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+1
View File
@@ -112,6 +112,7 @@
"iso-3166": "^4.4.0",
"lucide-react": "^1.8.0",
"nostr-tools": "^2.13.0",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"react": "^19.2.4",
"react-blurhash": "^0.3.0",
+31 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Loader2, Search } from 'lucide-react';
import { Loader2, QrCode, Search } from 'lucide-react';
import { nip19 } from 'nostr-tools';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
@@ -10,12 +10,21 @@ import { useAuthor } from '@/hooks/useAuthor';
import { useSearchProfiles, type SearchProfile } from '@/hooks/useSearchProfiles';
import { genUserName } from '@/lib/genUserName';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { cn } from '@/lib/utils';
interface BitcoinRecipientInputProps {
value: string;
onChange: (value: string) => void;
placeholder: string;
resolvedPubkey?: string;
/**
* When provided, a camera button is rendered inside the input that opens
* a QR scanner. The parent owns the scanner lifecycle and interprets the
* scan result (BIP-21 parsing, silent-payment priority, etc.).
*/
onScanClick?: () => void;
/** Localized aria-label for the scan button (only used when `onScanClick` is set). */
scanLabel?: string;
}
function shouldSkipProfileSearch(value: string): boolean {
@@ -33,7 +42,7 @@ function shouldSkipProfileSearch(value: string): boolean {
);
}
export function BitcoinRecipientInput({ value, onChange, placeholder, resolvedPubkey }: BitcoinRecipientInputProps) {
export function BitcoinRecipientInput({ value, onChange, placeholder, resolvedPubkey, onScanClick, scanLabel }: BitcoinRecipientInputProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const searchQuery = shouldSkipProfileSearch(value) ? '' : value;
@@ -67,7 +76,12 @@ export function BitcoinRecipientInput({ value, onChange, placeholder, resolvedPu
<div className="relative flex items-center">
<Search className="absolute left-3 size-4 text-muted-foreground pointer-events-none" />
{isFetching && shouldShowSearch && (
<Loader2 className="absolute right-3 size-4 text-muted-foreground animate-spin" />
<Loader2
className={cn(
'absolute size-4 text-muted-foreground animate-spin',
onScanClick ? 'right-11' : 'right-3',
)}
/>
)}
<Input
ref={inputRef}
@@ -80,8 +94,21 @@ export function BitcoinRecipientInput({ value, onChange, placeholder, resolvedPu
placeholder={placeholder}
autoComplete="off"
spellCheck={false}
className="pl-10 pr-10 font-mono text-base md:text-sm"
className={cn(
'pl-10 font-mono text-base md:text-sm',
onScanClick ? 'pr-11' : 'pr-10',
)}
/>
{onScanClick && (
<button
type="button"
onClick={onScanClick}
aria-label={scanLabel ?? 'Scan QR code'}
className="absolute right-1 top-1/2 -translate-y-1/2 size-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-secondary/60 flex items-center justify-center motion-safe:transition-colors"
>
<QrCode className="size-4" />
</button>
)}
</div>
</PopoverTrigger>
+40
View File
@@ -26,6 +26,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { BitcoinAmountPicker } from '@/components/BitcoinAmountPicker';
import { BitcoinPublicDisclaimer } from '@/components/BitcoinPublicDisclaimer';
import { BitcoinRecipientInput } from '@/components/BitcoinRecipientInput';
import { QrScannerDialog } from '@/components/QrScannerDialog';
import { HelpTip } from '@/components/HelpTip';
import { cn } from '@/lib/utils';
@@ -42,6 +43,7 @@ import {
import {
isLargeAmount,
nostrPubkeyToBitcoinAddress,
parseBitcoinUri,
satsToUSD,
} from '@/lib/bitcoin';
import {
@@ -228,11 +230,39 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
const [error, setError] = useState('');
const [feePopoverOpen, setFeePopoverOpen] = useState(false);
const [success, setSuccess] = useState<SendResult | null>(null);
const [scannerOpen, setScannerOpen] = useState(false);
const feeSpeedUserChanged = useRef(false);
const recipient = useMemo(() => resolveRecipient(recipientInput), [recipientInput]);
/**
* Interpret a freshly-scanned QR code and stuff it into the recipient
* input. A `bitcoin:bc1q…?sp=sp1q…` BIP-21 URI means "send via silent
* payment if you can; otherwise fall back to the on-chain address" — we
* prefer the `sp` parameter when it parses, and the swap toggle under the
* input lets the user fall back to the on-chain address if they want to.
* Anything else (bare address, `sp1…`, npub, nprofile) is dropped in
* verbatim and `resolveRecipient` does the rest.
*/
const handleScan = useCallback((scanned: string) => {
setScannerOpen(false);
setError('');
const trimmed = scanned.trim();
const bip21 = parseBitcoinUri(trimmed);
if (bip21) {
if (bip21.sp && resolveRecipient(bip21.sp)) {
setRecipientInput(bip21.sp);
return;
}
if (bip21.address) {
setRecipientInput(bip21.address);
return;
}
}
setRecipientInput(trimmed);
}, []);
// ── Fee rates ────────────────────────────────────────────────
const { data: feeRates } = useQuery({
queryKey: ['blockbook-fee-rates', blockbookBaseUrl],
@@ -533,6 +563,7 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
// ── Render ───────────────────────────────────────────────────
return (
<>
<Dialog open={isOpen} onOpenChange={(v) => { if (!v) handleClose(); }}>
<DialogContent className="max-w-[425px] rounded-2xl p-0 gap-0 border-border overflow-hidden max-h-[95vh] [&>button]:hidden">
<DialogTitle className="sr-only">{t('walletSend.title')}</DialogTitle>
@@ -583,6 +614,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
onChange={setRecipientInput}
placeholder={t('walletSend.recipient.placeholder')}
resolvedPubkey={recipient?.pubkey}
onScanClick={() => setScannerOpen(true)}
scanLabel={t('walletSend.recipient.scan')}
/>
{recipient && (
<p className="text-xs text-muted-foreground">
@@ -703,6 +736,13 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien
)}
</DialogContent>
</Dialog>
<QrScannerDialog
isOpen={scannerOpen}
onClose={() => setScannerOpen(false)}
onScan={handleScan}
title={t('walletSend.recipient.scan')}
/>
</>
);
}
+311
View File
@@ -0,0 +1,311 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Camera, Loader2, X, ZapOff, Zap } from 'lucide-react';
import QrScanner from 'qr-scanner';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { cn } from '@/lib/utils';
interface QrScannerDialogProps {
isOpen: boolean;
onClose: () => void;
/** Called with the decoded QR text the first time a code is read. */
onScan: (value: string) => void;
/** Override the dialog title (defaults to the localized "Scan QR code"). */
title?: string;
}
/** How long to wait after `start()` resolves before declaring the camera dead. */
const VIDEO_READY_TIMEOUT_MS = 6000;
/**
* Camera-based QR scanner dialog. Works in browsers, Capacitor's WKWebView
* (iOS), and Android's WebView, all via `getUserMedia` + the `qr-scanner`
* library (ZXing / BarcodeDetector under the hood).
*
* The dialog owns the camera lifecycle: it spins up the scanner when opened
* and tears it down on close, so callers only need to manage `isOpen` and
* react to `onScan`.
*
* Failure modes we explicitly surface (instead of a silent black screen):
* - Insecure context (HTTP) — getUserMedia is unavailable.
* - Camera permission denied.
* - No camera on the device.
* - `facingMode: 'environment'` not satisfiable (some laptops, some
* locked-down WebViews). We retry with the front camera.
* - `start()` resolves but the video never emits `loadedmetadata` within
* `VIDEO_READY_TIMEOUT_MS` — usually means the worker engine failed to
* initialize or another app is holding the camera.
*/
export function QrScannerDialog({ isOpen, onClose, onScan, title }: QrScannerDialogProps) {
const { t } = useTranslation();
// Callback ref so we know the moment the element is attached. Radix
// Dialog mounts content lazily inside a Portal, so a plain `useRef` is
// still null on the first effect tick after `isOpen` flips to true.
// A state-backed ref re-runs the effect once the <video> actually
// exists in the DOM.
const [video, setVideo] = useState<HTMLVideoElement | null>(null);
const scannerRef = useRef<QrScanner | null>(null);
// Keep the latest onScan in a ref so the start effect doesn't tear down
// the camera every time the parent passes a new callback identity.
const onScanRef = useRef(onScan);
onScanRef.current = onScan;
const [status, setStatus] = useState<'idle' | 'starting' | 'running' | 'error'>('idle');
const [error, setError] = useState<string | null>(null);
const [hasFlash, setHasFlash] = useState(false);
const [flashOn, setFlashOn] = useState(false);
useEffect(() => {
if (!isOpen) return;
if (!video) return;
// Secure-context guard — getUserMedia is only available on https / localhost.
if (typeof window !== 'undefined' && window.isSecureContext === false) {
setStatus('error');
setError(t('qrScanner.errors.insecure'));
return;
}
if (!navigator.mediaDevices?.getUserMedia) {
setStatus('error');
setError(t('qrScanner.errors.unsupported'));
return;
}
let cancelled = false;
let readyTimer: ReturnType<typeof setTimeout> | undefined;
setStatus('starting');
setError(null);
const handleDecode = (result: QrScanner.ScanResult) => {
if (cancelled) return;
cancelled = true;
scanner.stop();
onScanRef.current(result.data);
};
let scanner: QrScanner;
try {
scanner = new QrScanner(
video,
handleDecode,
{
returnDetailedScanResult: true,
highlightScanRegion: true,
highlightCodeOutline: true,
preferredCamera: 'environment',
maxScansPerSecond: 5,
},
);
} catch (err) {
setStatus('error');
setError(humanizeCameraError(err, t));
return;
}
scannerRef.current = scanner;
/**
* Watch the video element. If it never reaches `HAVE_METADATA` within the
* timeout, the scanner is silently broken. Surface that instead of a
* black screen.
*/
const armReadyTimeout = () => {
readyTimer = setTimeout(() => {
if (cancelled) return;
if (video.readyState < 1 /* HAVE_METADATA */) {
cancelled = true;
scanner.stop();
setStatus('error');
setError(t('qrScanner.errors.didntStart'));
}
}, VIDEO_READY_TIMEOUT_MS);
};
const clearReadyTimeout = () => {
if (readyTimer) {
clearTimeout(readyTimer);
readyTimer = undefined;
}
};
const onLoadedMetadata = () => clearReadyTimeout();
video.addEventListener('loadedmetadata', onLoadedMetadata, { once: true });
/**
* Some devices reject `facingMode: 'environment'` with OverconstrainedError
* (laptops without a rear camera, some Android WebViews). Retry without a
* camera preference so the browser picks any available device.
*/
const startWithFallback = async () => {
try {
await scanner.start();
} catch (err) {
if (cancelled) return;
if (isOverconstrainedError(err)) {
try {
await scanner.setCamera('user');
return;
} catch {
// Fall through and report the original error.
}
}
throw err;
}
};
startWithFallback()
.then(async () => {
if (cancelled) return;
clearReadyTimeout();
setStatus('running');
armReadyTimeout();
try {
const flashAvailable = await scanner.hasFlash();
if (!cancelled) setHasFlash(flashAvailable);
} catch {
// Flash detection is best-effort; ignore.
}
})
.catch((err: unknown) => {
if (cancelled) return;
clearReadyTimeout();
setStatus('error');
setError(humanizeCameraError(err, t));
});
// Arm an initial timeout in case `start()` neither resolves nor rejects
// (e.g. the worker engine wedges on CSP-blocked blob creation, or the
// OS permission dialog is dismissed without a callback firing).
armReadyTimeout();
return () => {
cancelled = true;
clearReadyTimeout();
video.removeEventListener('loadedmetadata', onLoadedMetadata);
scanner.stop();
scanner.destroy();
scannerRef.current = null;
setHasFlash(false);
setFlashOn(false);
};
}, [isOpen, video, t]);
const toggleFlash = async () => {
const scanner = scannerRef.current;
if (!scanner) return;
try {
await scanner.toggleFlash();
setFlashOn(scanner.isFlashOn());
} catch {
// Ignore — some devices report `hasFlash` true but error on toggle.
}
};
return (
<Dialog open={isOpen} onOpenChange={(open) => { if (!open) onClose(); }}>
<DialogContent className="max-w-[min(380px,calc(100vw-2rem))] max-h-[calc(100svh-2rem)] rounded-2xl p-0 gap-0 border-border overflow-hidden flex flex-col [&>button]:hidden">
<div className="flex items-center justify-between px-4 h-12 shrink-0">
<DialogTitle className="text-base font-semibold flex items-center gap-1.5">
{title ?? t('qrScanner.title')}
</DialogTitle>
<button
onClick={onClose}
className="p-1.5 -mr-1.5 rounded-full text-muted-foreground hover:text-foreground hover:bg-secondary/60 motion-safe:transition-colors"
aria-label={t('qrScanner.close')}
>
<X className="size-5" />
</button>
</div>
<div
className="relative w-full aspect-square bg-black overflow-hidden shrink-0"
style={{ maxHeight: 'min(380px, calc(100vw - 2rem))' }}
>
<video
ref={setVideo}
className={cn(
'absolute inset-0 w-full h-full object-cover',
status !== 'running' && 'opacity-0',
)}
playsInline
muted
/>
{status === 'starting' && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-white/80">
<Loader2 className="size-8 animate-spin" />
<p className="text-sm">{t('qrScanner.starting')}</p>
</div>
)}
{status === 'error' && (
<div className="absolute inset-0 flex items-center justify-center p-6">
<Alert variant="destructive" className="bg-background">
<Camera className="size-4" />
<AlertDescription className="text-xs">
{error || t('qrScanner.errors.generic')}
</AlertDescription>
</Alert>
</div>
)}
{status === 'running' && hasFlash && (
<button
type="button"
onClick={toggleFlash}
aria-label={flashOn ? t('qrScanner.flashOff') : t('qrScanner.flashOn')}
className="absolute bottom-3 right-3 size-10 rounded-full bg-black/50 backdrop-blur-sm text-white flex items-center justify-center hover:bg-black/70 motion-safe:transition-colors"
>
{flashOn ? <ZapOff className="size-5" /> : <Zap className="size-5" />}
</button>
)}
</div>
<div className="px-4 py-3 shrink-0">
<p className="text-xs text-muted-foreground text-center">
{t('qrScanner.aim')}
</p>
{status === 'error' && (
<Button onClick={onClose} variant="outline" className="w-full mt-3">
{t('qrScanner.close')}
</Button>
)}
</div>
</DialogContent>
</Dialog>
);
}
function isOverconstrainedError(err: unknown): boolean {
if (!err || typeof err !== 'object') return false;
const name = (err as { name?: unknown }).name;
if (name === 'OverconstrainedError') return true;
const msg = err instanceof Error ? err.message : '';
return /overconstrained|constraint/i.test(msg);
}
function humanizeCameraError(err: unknown, t: (key: string) => string): string {
const name = (err && typeof err === 'object' ? (err as { name?: unknown }).name : undefined);
const msg = err instanceof Error ? err.message : String(err);
if (name === 'NotFoundError' || /no camera/i.test(msg) || /not found/i.test(msg)) {
return t('qrScanner.errors.notFound');
}
if (name === 'NotAllowedError' || /permission/i.test(msg) || /denied/i.test(msg)) {
return t('qrScanner.errors.denied');
}
if (name === 'NotReadableError' || /in use|busy|readable/i.test(msg)) {
return t('qrScanner.errors.busy');
}
if (name === 'SecurityError' || /secure context/i.test(msg) || /https/i.test(msg)) {
return t('qrScanner.errors.insecure');
}
if (name === 'OverconstrainedError' || /overconstrained|constraint/i.test(msg)) {
return t('qrScanner.errors.overconstrained');
}
return msg || t('qrScanner.errors.generic');
}
+42
View File
@@ -584,6 +584,48 @@ export function validateBitcoinAddress(address: string): boolean {
}
}
/**
* Parsed shape of a `bitcoin:` BIP-21 URI.
*
* Only the fields the wallet actually consumes are surfaced. The amount,
* label, and message parameters are ignored — the Send dialog lets the user
* pick the USD amount, and we have no lightning fallback to consume.
*/
export interface ParsedBitcoinUri {
/** On-chain address from the URI path. May be empty for sp-only URIs. */
address: string;
/** BIP-352 silent payment address from the `sp=` parameter, if present. */
sp?: string;
}
/**
* Parse a `bitcoin:` BIP-21 URI into its address + optional silent-payment
* fallback. Returns `null` for anything that isn't `bitcoin:…` (the scheme
* check is case-insensitive).
*
* Validation of the address / `sp` values is left to the caller — this
* helper just splits the URI. A BIP-21 URI like
* `bitcoin:bc1q…?sp=sp1q…` is interpreted by callers as "send via silent
* payment if you can; otherwise fall back to the on-chain address".
*/
export function parseBitcoinUri(input: string): ParsedBitcoinUri | null {
const trimmed = input.trim();
if (!/^bitcoin:/i.test(trimmed)) return null;
const payload = trimmed.slice('bitcoin:'.length);
const qIdx = payload.indexOf('?');
const address = (qIdx === -1 ? payload : payload.slice(0, qIdx)).trim();
let sp: string | undefined;
if (qIdx !== -1) {
// URLSearchParams handles percent-decoding and repeated keys.
const params = new URLSearchParams(payload.slice(qIdx + 1));
sp = params.get('sp')?.trim() || undefined;
}
return { address, sp };
}
/**
* Broadcast a signed transaction hex to the Bitcoin network via an
* Esplora-compatible API. Returns the txid.
+20 -1
View File
@@ -1056,7 +1056,8 @@
"sendingNostr": "الإرسال إلى عنوان مستخدم Nostr على السلسلة.",
"sendingRaw": "الإرسال إلى عنوان بيتكوين خام.",
"useSilentPayment": "استخدام الدفع الصامت بدلاً من ذلك",
"useOnchain": "استخدام عنوان على السلسلة بدلاً من ذلك"
"useOnchain": "استخدام عنوان على السلسلة بدلاً من ذلك",
"scan": "مسح رمز QR"
},
"feeSpeed": {
"fastest": "~10 دقائق",
@@ -1092,6 +1093,24 @@
"done": "تم"
}
},
"qrScanner": {
"title": "مسح رمز QR",
"close": "إغلاق",
"starting": "جارٍ تشغيل الكاميرا…",
"aim": "وجّه الكاميرا نحو رمز QR.",
"flashOn": "تشغيل الفلاش",
"flashOff": "إيقاف الفلاش",
"errors": {
"generic": "تعذّر الوصول إلى الكاميرا.",
"insecure": "يتطلب الوصول إلى الكاميرا اتصالاً آمناً (HTTPS).",
"unsupported": "هذا المتصفح لا يدعم الوصول إلى الكاميرا.",
"notFound": "لم يُعثر على أي كاميرا في هذا الجهاز.",
"denied": "تم رفض إذن الكاميرا. فعّله من الإعدادات لمسح رموز QR.",
"busy": "تطبيق آخر يستخدم الكاميرا. أغلقه وحاول مجدداً.",
"overconstrained": "كاميرا هذا الجهاز لا تدعم الإعدادات المطلوبة.",
"didntStart": "لم تبدأ الكاميرا. ربما يستخدمها تطبيق آخر، أو ربما حظر متصفحك الماسح."
}
},
"bitcoinPublic": {
"lead": "الأموال التي ترسلها علنية ويمكن تتبّعها إليك.",
"learnMore": "اعرف المزيد",
+20 -1
View File
@@ -1495,7 +1495,8 @@
"sendingNostr": "Sending to a Nostr user's on-chain address.",
"sendingRaw": "Sending to a raw Bitcoin address.",
"useSilentPayment": "Use silent payment instead",
"useOnchain": "Use on-chain address instead"
"useOnchain": "Use on-chain address instead",
"scan": "Scan QR code"
},
"feeSpeed": {
"fastest": "~10 min",
@@ -1531,6 +1532,24 @@
"done": "Done"
}
},
"qrScanner": {
"title": "Scan QR code",
"close": "Close",
"starting": "Starting camera…",
"aim": "Point your camera at a QR code.",
"flashOn": "Turn flash on",
"flashOff": "Turn flash off",
"errors": {
"generic": "Could not access the camera.",
"insecure": "Camera access requires a secure (HTTPS) connection.",
"unsupported": "This browser doesn't support camera access.",
"notFound": "No camera was found on this device.",
"denied": "Camera permission was denied. Enable it in your settings to scan QR codes.",
"busy": "Another app is using the camera. Close it and try again.",
"overconstrained": "This device's camera doesn't support the requested settings.",
"didntStart": "Camera didn't start. Another app may be using it, or your browser may have blocked the scanner."
}
},
"bitcoinPublic": {
"lead": "Money you send is public and can be traced back to you.",
"learnMore": "Learn more",
+20 -1
View File
@@ -1072,7 +1072,8 @@
"sendingNostr": "Enviando a la dirección en cadena de un usuario de Nostr.",
"sendingRaw": "Enviando a una dirección de Bitcoin sin asociar.",
"useSilentPayment": "Usar pago silencioso en su lugar",
"useOnchain": "Usar dirección en cadena en su lugar"
"useOnchain": "Usar dirección en cadena en su lugar",
"scan": "Escanear código QR"
},
"feeSpeed": {
"fastest": "~10 min",
@@ -1108,6 +1109,24 @@
"done": "Hecho"
}
},
"qrScanner": {
"title": "Escanear código QR",
"close": "Cerrar",
"starting": "Iniciando la cámara…",
"aim": "Apunta la cámara al código QR.",
"flashOn": "Encender el flash",
"flashOff": "Apagar el flash",
"errors": {
"generic": "No se pudo acceder a la cámara.",
"insecure": "El acceso a la cámara requiere una conexión segura (HTTPS).",
"unsupported": "Este navegador no admite el acceso a la cámara.",
"notFound": "No se encontró ninguna cámara en este dispositivo.",
"denied": "Se denegó el permiso de la cámara. Actívalo en los ajustes para escanear códigos QR.",
"busy": "Otra aplicación está usando la cámara. Ciérrala e inténtalo de nuevo.",
"overconstrained": "La cámara de este dispositivo no admite la configuración solicitada.",
"didntStart": "La cámara no se inició. Es posible que otra aplicación la esté usando o que el navegador haya bloqueado el escáner."
}
},
"bitcoinPublic": {
"lead": "El dinero que envías es público y puede rastrearse hasta ti.",
"learnMore": "Más información",
+20 -1
View File
@@ -1072,7 +1072,8 @@
"sendingNostr": "ارسال به نشانی روی‌زنجیرهٔ یک کاربر Nostr.",
"sendingRaw": "ارسال به یک نشانی خام بیت‌کوین.",
"useSilentPayment": "استفاده از پرداخت خاموش به جای آن",
"useOnchain": "استفاده از نشانی روی‌زنجیره به جای آن"
"useOnchain": "استفاده از نشانی روی‌زنجیره به جای آن",
"scan": "اسکن کد QR"
},
"feeSpeed": {
"fastest": "~۱۰ دقیقه",
@@ -1108,6 +1109,24 @@
"done": "انجام شد"
}
},
"qrScanner": {
"title": "اسکن کد QR",
"close": "بستن",
"starting": "در حال راه‌اندازی دوربین…",
"aim": "دوربین خود را به سمت کد QR بگیرید.",
"flashOn": "روشن کردن فلاش",
"flashOff": "خاموش کردن فلاش",
"errors": {
"generic": "دسترسی به دوربین ممکن نشد.",
"insecure": "دسترسی به دوربین به یک اتصال امن (HTTPS) نیاز دارد.",
"unsupported": "این مرورگر از دسترسی به دوربین پشتیبانی نمی‌کند.",
"notFound": "هیچ دوربینی روی این دستگاه پیدا نشد.",
"denied": "اجازهٔ دسترسی به دوربین رد شد. برای اسکن کدهای QR آن را در تنظیمات فعال کنید.",
"busy": "برنامهٔ دیگری از دوربین استفاده می‌کند. آن را ببندید و دوباره تلاش کنید.",
"overconstrained": "دوربین این دستگاه از تنظیمات درخواست‌شده پشتیبانی نمی‌کند.",
"didntStart": "دوربین شروع به کار نکرد. ممکن است برنامهٔ دیگری از آن استفاده کند، یا مرورگر شما اسکنر را مسدود کرده باشد."
}
},
"bitcoinPublic": {
"lead": "پولی که ارسال می‌کنید عمومی است و می‌تواند به شما بازیابی شود.",
"learnMore": "بیشتر بدانید",
+20 -1
View File
@@ -1494,7 +1494,8 @@
"sendingNostr": "Envoi vers l'adresse on-chain d'un utilisateur Nostr.",
"sendingRaw": "Envoi vers une adresse Bitcoin brute.",
"useSilentPayment": "Utiliser un paiement silencieux à la place",
"useOnchain": "Utiliser une adresse on-chain à la place"
"useOnchain": "Utiliser une adresse on-chain à la place",
"scan": "Scanner le QR code"
},
"feeSpeed": {
"fastest": "~10 min",
@@ -1530,6 +1531,24 @@
"done": "Terminé"
}
},
"qrScanner": {
"title": "Scanner le QR code",
"close": "Fermer",
"starting": "Démarrage de la caméra…",
"aim": "Pointez votre caméra vers un QR code.",
"flashOn": "Activer le flash",
"flashOff": "Désactiver le flash",
"errors": {
"generic": "Impossible d'accéder à la caméra.",
"insecure": "L'accès à la caméra nécessite une connexion sécurisée (HTTPS).",
"unsupported": "Ce navigateur ne prend pas en charge l'accès à la caméra.",
"notFound": "Aucune caméra n'a été trouvée sur cet appareil.",
"denied": "L'autorisation de la caméra a été refusée. Activez-la dans vos paramètres pour scanner des QR codes.",
"busy": "Une autre application utilise la caméra. Fermez-la et réessayez.",
"overconstrained": "La caméra de cet appareil ne prend pas en charge les paramètres demandés.",
"didntStart": "La caméra n'a pas démarré. Une autre application l'utilise peut-être, ou votre navigateur a peut-être bloqué le scanner."
}
},
"bitcoinPublic": {
"lead": "L'argent que vous envoyez est public et peut être retracé jusqu'à vous.",
"learnMore": "En savoir plus",
+20 -1
View File
@@ -1440,7 +1440,8 @@
"sendingNostr": "एक Nostr यूज़र के ऑन-चेन एड्रेस पर भेजा जा रहा है।",
"sendingRaw": "एक raw Bitcoin एड्रेस पर भेजा जा रहा है।",
"useSilentPayment": "इसके बजाय साइलेंट पेमेंट का उपयोग करें",
"useOnchain": "इसके बजाय ऑन-चेन एड्रेस का उपयोग करें"
"useOnchain": "इसके बजाय ऑन-चेन एड्रेस का उपयोग करें",
"scan": "QR कोड स्कैन करें"
},
"feeSpeed": {
"fastest": "~10 मिनट",
@@ -1476,6 +1477,24 @@
"done": "हो गया"
}
},
"qrScanner": {
"title": "QR कोड स्कैन करें",
"close": "बंद करें",
"starting": "कैमरा शुरू हो रहा है…",
"aim": "अपने कैमरे को QR कोड पर ले जाएँ।",
"flashOn": "फ़्लैश चालू करें",
"flashOff": "फ़्लैश बंद करें",
"errors": {
"generic": "कैमरे तक नहीं पहुँचा जा सका।",
"insecure": "कैमरा एक्सेस के लिए सुरक्षित (HTTPS) कनेक्शन ज़रूरी है।",
"unsupported": "यह ब्राउज़र कैमरा एक्सेस को सपोर्ट नहीं करता।",
"notFound": "इस डिवाइस पर कोई कैमरा नहीं मिला।",
"denied": "कैमरे की अनुमति अस्वीकृत कर दी गई। QR कोड स्कैन करने के लिए इसे अपनी सेटिंग्स में चालू करें।",
"busy": "कोई दूसरा ऐप कैमरे का इस्तेमाल कर रहा है। उसे बंद करके दोबारा कोशिश करें।",
"overconstrained": "इस डिवाइस का कैमरा अनुरोधित सेटिंग्स को सपोर्ट नहीं करता।",
"didntStart": "कैमरा शुरू नहीं हुआ। कोई दूसरा ऐप इसका उपयोग कर रहा हो सकता है, या आपके ब्राउज़र ने स्कैनर को ब्लॉक कर दिया हो।"
}
},
"bitcoinPublic": {
"lead": "आप जो पैसा भेजते हैं वह सार्वजनिक है और आप तक trace किया जा सकता है।",
"learnMore": "और जानें",
+20 -1
View File
@@ -1440,7 +1440,8 @@
"sendingNostr": "Mengirim ke alamat on-chain pengguna Nostr.",
"sendingRaw": "Mengirim ke alamat Bitcoin mentah.",
"useSilentPayment": "Gunakan silent payment saja",
"useOnchain": "Gunakan alamat on-chain saja"
"useOnchain": "Gunakan alamat on-chain saja",
"scan": "Pindai kode QR"
},
"feeSpeed": {
"fastest": "~10 menit",
@@ -1476,6 +1477,24 @@
"done": "Selesai"
}
},
"qrScanner": {
"title": "Pindai kode QR",
"close": "Tutup",
"starting": "Menyalakan kamera…",
"aim": "Arahkan kamera Anda ke kode QR.",
"flashOn": "Nyalakan lampu kilat",
"flashOff": "Matikan lampu kilat",
"errors": {
"generic": "Tidak bisa mengakses kamera.",
"insecure": "Akses kamera memerlukan koneksi yang aman (HTTPS).",
"unsupported": "Peramban ini tidak mendukung akses kamera.",
"notFound": "Tidak ada kamera yang ditemukan di perangkat ini.",
"denied": "Izin kamera ditolak. Aktifkan di pengaturan untuk memindai kode QR.",
"busy": "Aplikasi lain sedang menggunakan kamera. Tutup dan coba lagi.",
"overconstrained": "Kamera perangkat ini tidak mendukung pengaturan yang diminta.",
"didntStart": "Kamera tidak menyala. Mungkin aplikasi lain sedang menggunakannya, atau peramban Anda memblokir pemindai."
}
},
"bitcoinPublic": {
"lead": "Uang yang Anda kirim bersifat publik dan bisa dilacak kembali ke Anda.",
"learnMore": "Pelajari lebih lanjut",
+20 -1
View File
@@ -1072,7 +1072,8 @@
"sendingNostr": "ផ្ញើទៅអាសយដ្ឋានលើខ្សែសង្វាក់របស់អ្នកប្រើ Nostr ម្នាក់។",
"sendingRaw": "ផ្ញើទៅអាសយដ្ឋាន Bitcoin ដើម។",
"useSilentPayment": "ប្រើការទូទាត់ស្ងាត់ជំនួសវិញ",
"useOnchain": "ប្រើអាសយដ្ឋានលើខ្សែសង្វាក់ជំនួសវិញ"
"useOnchain": "ប្រើអាសយដ្ឋានលើខ្សែសង្វាក់ជំនួសវិញ",
"scan": "ស្កេនកូដ QR"
},
"feeSpeed": {
"fastest": "~១០ នាទី",
@@ -1108,6 +1109,24 @@
"done": "រួចរាល់"
}
},
"qrScanner": {
"title": "ស្កេនកូដ QR",
"close": "បិទ",
"starting": "កំពុងចាប់ផ្ដើមកាមេរ៉ា…",
"aim": "តម្រង់កាមេរ៉ារបស់អ្នកទៅកាន់កូដ QR។",
"flashOn": "បើកពិល",
"flashOff": "បិទពិល",
"errors": {
"generic": "មិនអាចចូលប្រើកាមេរ៉ាបានទេ។",
"insecure": "ការចូលប្រើកាមេរ៉ាទាមទារការតភ្ជាប់សុវត្ថិភាព (HTTPS)។",
"unsupported": "កម្មវិធីរុករកនេះមិនគាំទ្រការចូលប្រើកាមេរ៉ាទេ។",
"notFound": "រកមិនឃើញកាមេរ៉ានៅលើឧបករណ៍នេះទេ។",
"denied": "ការអនុញ្ញាតកាមេរ៉ាត្រូវបានបដិសេធ។ បើកវានៅក្នុងការកំណត់របស់អ្នកដើម្បីស្កេនកូដ QR។",
"busy": "កម្មវិធីផ្សេងទៀតកំពុងប្រើកាមេរ៉ា។ បិទវាហើយព្យាយាមម្តងទៀត។",
"overconstrained": "កាមេរ៉ានៃឧបករណ៍នេះមិនគាំទ្រការកំណត់ដែលបានស្នើទេ។",
"didntStart": "កាមេរ៉ាមិនបានចាប់ផ្ដើមទេ។ កម្មវិធីផ្សេងទៀតប្រហែលជាកំពុងប្រើវា ឬកម្មវិធីរុករករបស់អ្នកប្រហែលជាបានរារាំងម៉ាស៊ីនស្កេន។"
}
},
"bitcoinPublic": {
"lead": "ប្រាក់ដែលអ្នកផ្ញើគឺសាធារណៈ ហើយអាចត្រូវបានតាមដានទៅអ្នកវិញ។",
"learnMore": "ស្វែងយល់បន្ថែម",
+20 -1
View File
@@ -1072,7 +1072,8 @@
"sendingNostr": "د Nostr کارن د زنځیر په پته لیږل.",
"sendingRaw": "یوې خامه بټکوین پتې ته لیږل.",
"useSilentPayment": "پر ځای يې د چوپې ورکړې وکاروئ",
"useOnchain": "پر ځای يې د زنځیر په پته وکاروئ"
"useOnchain": "پر ځای يې د زنځیر په پته وکاروئ",
"scan": "د QR کوډ سکن کړئ"
},
"feeSpeed": {
"fastest": "~۱۰ دقیقې",
@@ -1108,6 +1109,24 @@
"done": "بشپړ"
}
},
"qrScanner": {
"title": "د QR کوډ سکن کړئ",
"close": "بندول",
"starting": "کیمره پیلیږي…",
"aim": "خپله کیمره د QR کوډ پر لور ونیسئ.",
"flashOn": "فلش بل کړئ",
"flashOff": "فلش مړ کړئ",
"errors": {
"generic": "کیمرې ته لاسرسی ممکن نه شو.",
"insecure": "کیمرې ته لاسرسي ته خوندي (HTTPS) اړیکې اړتیا ده.",
"unsupported": "دا براوزر کیمرې ته لاسرسی نه ملاتړي.",
"notFound": "په دې وسیله کې هېڅ کیمره ونه موندل شوه.",
"denied": "د کیمرې اجازه رد شوه. د QR کوډونو د سکن کولو لپاره یې په تنظیماتو کې فعاله کړئ.",
"busy": "بل اپلیکیشن کیمره کاروي. هغه وتړئ او بیا هڅه وکړئ.",
"overconstrained": "د دې وسیلې کیمره غوښتل شوي تنظیمات نه ملاتړي.",
"didntStart": "کیمره پیل نه شوه. کېدای شي بل اپلیکیشن یې کاروي، یا ستاسو براوزر سکنر بند کړی وي."
}
},
"bitcoinPublic": {
"lead": "هغه پیسې چې لیږئ عامه دي او تاسو ته بیرته تعقیب کیدلی شي.",
"learnMore": "نور زده کړئ",
+20 -1
View File
@@ -1504,7 +1504,8 @@
"sendingNostr": "Enviando para o endereço on-chain de um usuário Nostr.",
"sendingRaw": "Enviando para um endereço Bitcoin bruto.",
"useSilentPayment": "Usar pagamento silencioso",
"useOnchain": "Usar endereço on-chain"
"useOnchain": "Usar endereço on-chain",
"scan": "Escanear código QR"
},
"feeSpeed": {
"fastest": "~10 min",
@@ -1540,6 +1541,24 @@
"done": "Concluído"
}
},
"qrScanner": {
"title": "Escanear código QR",
"close": "Fechar",
"starting": "Iniciando a câmera…",
"aim": "Aponte a câmera para um código QR.",
"flashOn": "Ligar o flash",
"flashOff": "Desligar o flash",
"errors": {
"generic": "Não foi possível acessar a câmera.",
"insecure": "O acesso à câmera requer uma conexão segura (HTTPS).",
"unsupported": "Este navegador não suporta acesso à câmera.",
"notFound": "Nenhuma câmera foi encontrada neste dispositivo.",
"denied": "A permissão da câmera foi negada. Ative-a nas configurações para escanear códigos QR.",
"busy": "Outro aplicativo está usando a câmera. Feche-o e tente novamente.",
"overconstrained": "A câmera deste dispositivo não suporta as configurações solicitadas.",
"didntStart": "A câmera não iniciou. Outro aplicativo pode estar usando-a, ou seu navegador pode ter bloqueado o scanner."
}
},
"bitcoinPublic": {
"lead": "O dinheiro que você envia é público e pode ser rastreado até você.",
"learnMore": "Saiba mais",
+20 -1
View File
@@ -1504,7 +1504,8 @@
"sendingNostr": "Отправка на адрес пользователя Nostr в блокчейне.",
"sendingRaw": "Отправка на сырой Bitcoin-адрес.",
"useSilentPayment": "Использовать тихий платёж",
"useOnchain": "Использовать адрес в блокчейне"
"useOnchain": "Использовать адрес в блокчейне",
"scan": "Сканировать QR-код"
},
"feeSpeed": {
"fastest": "~10 мин",
@@ -1540,6 +1541,24 @@
"done": "Готово"
}
},
"qrScanner": {
"title": "Сканировать QR-код",
"close": "Закрыть",
"starting": "Запуск камеры…",
"aim": "Наведите камеру на QR-код.",
"flashOn": "Включить вспышку",
"flashOff": "Выключить вспышку",
"errors": {
"generic": "Не удалось получить доступ к камере.",
"insecure": "Доступ к камере требует защищённого (HTTPS) соединения.",
"unsupported": "Этот браузер не поддерживает доступ к камере.",
"notFound": "Камера на этом устройстве не найдена.",
"denied": "Доступ к камере запрещён. Разрешите его в настройках, чтобы сканировать QR-коды.",
"busy": "Камеру использует другое приложение. Закройте его и попробуйте снова.",
"overconstrained": "Камера этого устройства не поддерживает запрошенные настройки.",
"didntStart": "Камера не запустилась. Возможно, её использует другое приложение, или браузер заблокировал сканер."
}
},
"bitcoinPublic": {
"lead": "Отправляемые вами деньги публичны и могут быть отслежены до вас.",
"learnMore": "Узнать больше",
+20 -1
View File
@@ -1072,7 +1072,8 @@
"sendingNostr": "Kutumira kukero yepacheni yemushandisi weNostr.",
"sendingRaw": "Kutumira kukero yeBitcoin yakangosaina.",
"useSilentPayment": "Shandisa mubhadharo wakanyararira pachinzvimbo",
"useOnchain": "Shandisa kero yepacheni pachinzvimbo"
"useOnchain": "Shandisa kero yepacheni pachinzvimbo",
"scan": "Skena kodhi yeQR"
},
"feeSpeed": {
"fastest": "~10 maminitsi",
@@ -1108,6 +1109,24 @@
"done": "Zvapera"
}
},
"qrScanner": {
"title": "Skena kodhi yeQR",
"close": "Vhara",
"starting": "Kutanga kamera…",
"aim": "Nongedza kamera yako kukodhi yeQR.",
"flashOn": "Batidza mwenje",
"flashOff": "Dzima mwenje",
"errors": {
"generic": "Hazvina kukwanisika kushandisa kamera.",
"insecure": "Kushandisa kamera kunoda kubatana kwakachengeteka (HTTPS).",
"unsupported": "Browser iyi haitsigire kushandisa kamera.",
"notFound": "Hapana kamera yakawanikwa pamudziyo uyu.",
"denied": "Mvumo yekamera yakarambwa. Ibvumire mukurongedza kwako kuti uskene makodhi eQR.",
"busy": "Imwe app iri kushandisa kamera. Ivhare wozoedza zvakare.",
"overconstrained": "Kamera yemudziyo uyu haitsigire zvakumbirwa.",
"didntStart": "Kamera haina kutanga. Imwe app inogona kunge iri kuishandisa, kana browser yako yakanga yavhara skena."
}
},
"bitcoinPublic": {
"lead": "Mari yaunotumira inozivikanwa nemunhu wese uye inogona kudzokerwa kwauri.",
"learnMore": "Dzidza zvakawanda",
+20 -1
View File
@@ -1399,7 +1399,8 @@
"sendingNostr": "Inatuma kwa anwani ya katika-mnyororo ya mtumiaji wa Nostr.",
"sendingRaw": "Inatuma kwa anwani ghafi ya Bitcoin.",
"useSilentPayment": "Tumia malipo ya kimya badala yake",
"useOnchain": "Tumia anwani ya katika-mnyororo badala yake"
"useOnchain": "Tumia anwani ya katika-mnyororo badala yake",
"scan": "Changanua msimbo wa QR"
},
"feeSpeed": {
"fastest": "~dakika 10",
@@ -1435,6 +1436,24 @@
"done": "Imekamilika"
}
},
"qrScanner": {
"title": "Changanua msimbo wa QR",
"close": "Funga",
"starting": "Inawasha kamera…",
"aim": "Elekeza kamera yako kwa msimbo wa QR.",
"flashOn": "Washa mweko",
"flashOff": "Zima mweko",
"errors": {
"generic": "Haikuweza kufikia kamera.",
"insecure": "Ufikiaji wa kamera unahitaji muunganisho salama (HTTPS).",
"unsupported": "Kivinjari hiki hakitumii ufikiaji wa kamera.",
"notFound": "Hakuna kamera iliyopatikana kwenye kifaa hiki.",
"denied": "Idhini ya kamera imekataliwa. Iwashe katika mipangilio yako ili kuchanganua misimbo ya QR.",
"busy": "Programu nyingine inatumia kamera. Ifunge na ujaribu tena.",
"overconstrained": "Kamera ya kifaa hiki haitumii mipangilio iliyoombwa.",
"didntStart": "Kamera haikuanza. Programu nyingine inaweza kuwa inaitumia, au kivinjari chako kinaweza kuwa kimezuia kichanganuzi."
}
},
"bitcoinPublic": {
"lead": "Pesa unazotuma ni za umma na zinaweza kufuatiliwa hadi kwako.",
"learnMore": "Jifunze zaidi",
+20 -1
View File
@@ -1439,7 +1439,8 @@
"sendingNostr": "Bir Nostr kullanıcısının zincir üstü adresine gönderiliyor.",
"sendingRaw": "Ham bir Bitcoin adresine gönderiliyor.",
"useSilentPayment": "Bunun yerine sessiz ödeme kullan",
"useOnchain": "Bunun yerine zincir üstü adres kullan"
"useOnchain": "Bunun yerine zincir üstü adres kullan",
"scan": "QR kodu tara"
},
"feeSpeed": {
"fastest": "~10 dk",
@@ -1475,6 +1476,24 @@
"done": "Tamam"
}
},
"qrScanner": {
"title": "QR kodu tara",
"close": "Kapat",
"starting": "Kamera başlatılıyor…",
"aim": "Kameranızı bir QR koduna doğrultun.",
"flashOn": "Flaşı aç",
"flashOff": "Flaşı kapat",
"errors": {
"generic": "Kameraya erişilemedi.",
"insecure": "Kamera erişimi için güvenli (HTTPS) bir bağlantı gerekiyor.",
"unsupported": "Bu tarayıcı kamera erişimini desteklemiyor.",
"notFound": "Bu cihazda kamera bulunamadı.",
"denied": "Kamera izni reddedildi. QR kodlarını taramak için ayarlarınızdan etkinleştirin.",
"busy": "Başka bir uygulama kamerayı kullanıyor. Onu kapatıp tekrar deneyin.",
"overconstrained": "Bu cihazın kamerası istenen ayarları desteklemiyor.",
"didntStart": "Kamera başlamadı. Başka bir uygulama kullanıyor olabilir ya da tarayıcınız tarayıcıyı engellemiş olabilir."
}
},
"bitcoinPublic": {
"lead": "Gönderdiğiniz para herkese açıktır ve size kadar izlenebilir.",
"learnMore": "Daha fazla bilgi",
+20 -1
View File
@@ -1008,7 +1008,8 @@
"sendingNostr": "傳送到某個 Nostr 使用者的鏈上地址。",
"sendingRaw": "傳送到一個原始比特幣地址。",
"useSilentPayment": "改用靜默支付",
"useOnchain": "改用鏈上地址"
"useOnchain": "改用鏈上地址",
"scan": "掃描 QR 碼"
},
"feeSpeed": {
"fastest": "~10 分鐘",
@@ -1044,6 +1045,24 @@
"done": "完成"
}
},
"qrScanner": {
"title": "掃描 QR 碼",
"close": "關閉",
"starting": "正在啟動相機…",
"aim": "將相機對準 QR 碼。",
"flashOn": "開啟閃光燈",
"flashOff": "關閉閃光燈",
"errors": {
"generic": "無法存取相機。",
"insecure": "存取相機需要安全(HTTPS)連線。",
"unsupported": "此瀏覽器不支援存取相機。",
"notFound": "此裝置上未找到相機。",
"denied": "相機權限被拒絕。請在設定中啟用以掃描 QR 碼。",
"busy": "另一個應用程式正在使用相機。請關閉它後再試。",
"overconstrained": "此裝置的相機不支援所請求的設定。",
"didntStart": "相機未啟動。可能有其他應用程式正在使用它,或者你的瀏覽器阻止了掃描器。"
}
},
"bitcoinPublic": {
"lead": "你傳送的資金是公開的,可以追溯到你。",
"learnMore": "瞭解更多",
+20 -1
View File
@@ -1072,7 +1072,8 @@
"sendingNostr": "发送到某个 Nostr 用户的链上地址。",
"sendingRaw": "发送到一个原始比特币地址。",
"useSilentPayment": "改用静默支付",
"useOnchain": "改用链上地址"
"useOnchain": "改用链上地址",
"scan": "扫描二维码"
},
"feeSpeed": {
"fastest": "~10 分钟",
@@ -1108,6 +1109,24 @@
"done": "完成"
}
},
"qrScanner": {
"title": "扫描二维码",
"close": "关闭",
"starting": "正在启动摄像头…",
"aim": "将摄像头对准二维码。",
"flashOn": "打开闪光灯",
"flashOff": "关闭闪光灯",
"errors": {
"generic": "无法访问摄像头。",
"insecure": "访问摄像头需要安全(HTTPS)连接。",
"unsupported": "此浏览器不支持访问摄像头。",
"notFound": "此设备上未找到摄像头。",
"denied": "摄像头权限被拒绝。请在设置中启用以扫描二维码。",
"busy": "另一个应用正在使用摄像头。请关闭它后再试。",
"overconstrained": "此设备的摄像头不支持所请求的设置。",
"didntStart": "摄像头未启动。可能有其他应用正在使用它,或者你的浏览器阻止了扫描器。"
}
},
"bitcoinPublic": {
"lead": "你发送的资金是公开的,可以追溯到你。",
"learnMore": "了解更多",