diff --git a/src/components/HDSendBitcoinDialog.tsx b/src/components/HDSendBitcoinDialog.tsx index c01b8a43..d2d17e05 100644 --- a/src/components/HDSendBitcoinDialog.tsx +++ b/src/components/HDSendBitcoinDialog.tsx @@ -22,6 +22,7 @@ import { PopoverTrigger, } from '@/components/ui/popover'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Input } from '@/components/ui/input'; import { BitcoinAmountPicker } from '@/components/BitcoinAmountPicker'; import { BitcoinPublicDisclaimer } from '@/components/BitcoinPublicDisclaimer'; import { @@ -39,6 +40,7 @@ import { notificationSuccess } from '@/lib/haptics'; import { getBitcoinFeeRate, getUniqueBitcoinFeeSpeeds, + resolveBitcoinFeeRate, type BitcoinFeeSpeed, } from '@/lib/bitcoinFeeSpeed'; import { isLargeAmount, satsToUSD } from '@/lib/bitcoin'; @@ -136,6 +138,7 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien halfHour: t('walletSend.feeSpeed.halfHour'), hour: t('walletSend.feeSpeed.hour'), economy: t('walletSend.feeSpeed.economy'), + custom: t('walletSend.feeSpeed.custom'), }), [t], ); @@ -147,6 +150,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien const [recipient, setRecipient] = useState(null); const [usdAmount, setUsdAmount] = useState(5); const [feeSpeed, setFeeSpeed] = useState('halfHour'); + /** Raw text for the custom sat/vB rate input (only used when feeSpeed === 'custom'). */ + const [customFeeRate, setCustomFeeRate] = useState(''); const [error, setError] = useState(''); const [feePopoverOpen, setFeePopoverOpen] = useState(false); const [success, setSuccess] = useState(null); @@ -155,17 +160,22 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien // ── Fee rates ──────────────────────────────────────────────── - const { data: feeRates } = useQuery({ + const { + data: feeRates, + isLoading: feeRatesLoading, + isError: feeRatesError, + refetch: refetchFeeRates, + } = useQuery({ queryKey: ['blockbook-fee-rates', blockbookBaseUrl], queryFn: ({ signal }) => fetchFeeRates(blockbookBaseUrl, signal), enabled: isOpen && isReady, staleTime: 30_000, }); - const currentFeeRate = useMemo(() => { - if (!feeRates) return undefined; - return getBitcoinFeeRate(feeRates, feeSpeed); - }, [feeRates, feeSpeed]); + const currentFeeRate = useMemo( + () => resolveBitcoinFeeRate(feeSpeed, feeRates, customFeeRate), + [feeSpeed, feeRates, customFeeRate], + ); // ── Owned UTXO set ─────────────────────────────────────────── // @@ -245,7 +255,9 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien const handleFeeSpeedChange = useCallback((speed: FeeSpeed) => { feeSpeedUserChanged.current = true; setFeeSpeed(speed); - setFeePopoverOpen(false); + // Keep the popover open for 'custom' so the user can type a rate; close + // it for preset tiers since the choice is complete. + if (speed !== 'custom') setFeePopoverOpen(false); }, []); // ── Two-tap arm + raw-address disclaimer ───────────────────── @@ -286,11 +298,12 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien } if (!recipient) throw new Error(t('walletSend.errors.enterRecipient')); if (!ownedInputs.length) throw new Error(t('walletSend.errors.noSpendable')); - if (!feeRates) throw new Error(t('walletSend.errors.feesNotLoaded')); + if (feeSpeed !== 'custom' && !feeRates) throw new Error(t('walletSend.errors.feesNotLoaded')); if (amountSats <= 0) throw new Error(t('walletSend.errors.enterAmount')); if (insufficient) throw new Error(t('walletSend.errors.insufficient')); - const rate = getBitcoinFeeRate(feeRates, feeSpeed); + const rate = resolveBitcoinFeeRate(feeSpeed, feeRates, customFeeRate); + if (!rate || rate < 1) throw new Error(t('walletSend.errors.feeRateTooLow')); const nextChangeIndex = scan?.change.firstUnusedIndex ?? 0; setProgress('building'); @@ -353,6 +366,14 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien if (!btcPrice) { setError(t('walletSend.errors.waitingPrice')); return; } if (amountSats <= 0) { setError(t('walletSend.errors.enterAmount')); return; } if (!ownedInputs.length) { setError(t('walletSend.errors.noneYet')); return; } + if (!currentFeeRate || currentFeeRate < 1) { + setError( + feeSpeed === 'custom' + ? t('walletSend.errors.feeRateTooLow') + : t('walletSend.errors.feesNotLoadedYet'), + ); + return; + } if (insufficient) { setError(t('walletSend.errors.insufficient')); return; } if (requiresArm && !confirmArmed) { setConfirmArmed(true); return; } sendMutation.mutate(); @@ -363,6 +384,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien btcPrice, amountSats, ownedInputs.length, + currentFeeRate, + feeSpeed, insufficient, requiresArm, confirmArmed, @@ -378,6 +401,8 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien setRecipient(null); setUsdAmount(5); setError(''); + setFeeSpeed('halfHour'); + setCustomFeeRate(''); setConfirmArmed(false); setSuccess(null); feeSpeedUserChanged.current = false; @@ -405,7 +430,9 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien !btcPrice || amountSats <= 0 || insufficient || - !ownedInputs.length; + !ownedInputs.length || + !currentFeeRate || + currentFeeRate < 1; // ── Render ─────────────────────────────────────────────────── return ( @@ -504,16 +531,41 @@ export function HDSendBitcoinDialog({ isOpen, onClose, btcPrice, initialRecipien <>≈ {satsToUSD(estimatedFeeSats, btcPrice)} ) : currentFeeRate ? ( <>{t('walletSend.satPerVB', { rate: currentFeeRate })} + ) : feeRatesLoading && feeSpeed !== 'custom' ? ( + <>{t('walletSend.fee.loading')} + ) : feeRatesError && feeSpeed !== 'custom' ? ( + <>{t('walletSend.fee.unavailable')} ) : ( <>— )} · - {feeSpeedLabels[feeSpeed]} + {feeSpeed === 'custom' && currentFeeRate + ? t('walletSend.satPerVB', { rate: currentFeeRate }) + : feeSpeedLabels[feeSpeed]} - +
- {getUniqueBitcoinFeeSpeeds(feeRates).map((speed) => ( + {feeRatesError && ( +
+

{t('walletSend.fee.loadFailed')}

+ +

{t('walletSend.fee.orCustom')}

+
+ )} + {feeRatesLoading && !feeRatesError && ( +
+ + {t('walletSend.fee.loadingTiers')} +
+ )} + {feeRates && getUniqueBitcoinFeeSpeeds(feeRates).map((speed) => ( ))} + {/* Custom fee rate */} + + {feeSpeed === 'custom' && ( +
+ setCustomFeeRate(e.target.value)} + placeholder={t('walletSend.fee.customPlaceholder')} + className="h-7 text-xs" + aria-label={t('walletSend.fee.customAriaLabel')} + /> + sat/vB +
+ )}
diff --git a/src/lib/bitcoinFeeSpeed.ts b/src/lib/bitcoinFeeSpeed.ts index 9b9253fa..05bbc323 100644 --- a/src/lib/bitcoinFeeSpeed.ts +++ b/src/lib/bitcoinFeeSpeed.ts @@ -1,6 +1,14 @@ export const BITCOIN_FEE_SPEED_ORDER = ['fastest', 'halfHour', 'hour', 'economy'] as const; -export type BitcoinFeeSpeed = typeof BITCOIN_FEE_SPEED_ORDER[number]; +/** The preset confirmation-speed tiers, in display order. */ +export type PresetBitcoinFeeSpeed = typeof BITCOIN_FEE_SPEED_ORDER[number]; + +/** + * A fee selection: one of the preset tiers, or `'custom'` for a + * user-entered sat/vB rate (used when the estimate API is down or the + * user wants explicit control). + */ +export type BitcoinFeeSpeed = PresetBitcoinFeeSpeed | 'custom'; export interface BitcoinFeeRates { fastestFee: number; @@ -9,7 +17,7 @@ export interface BitcoinFeeRates { economyFee: number; } -export function getBitcoinFeeRate(rates: BitcoinFeeRates, speed: BitcoinFeeSpeed): number { +export function getBitcoinFeeRate(rates: BitcoinFeeRates, speed: PresetBitcoinFeeSpeed): number { switch (speed) { case 'fastest': return rates.fastestFee; case 'halfHour': return rates.halfHourFee; @@ -20,10 +28,10 @@ export function getBitcoinFeeRate(rates: BitcoinFeeRates, speed: BitcoinFeeSpeed export function getUniqueBitcoinFeeSpeeds( rates: BitcoinFeeRates | undefined, -): BitcoinFeeSpeed[] { +): PresetBitcoinFeeSpeed[] { if (!rates) return [...BITCOIN_FEE_SPEED_ORDER]; const seen = new Set(); - const result: BitcoinFeeSpeed[] = []; + const result: PresetBitcoinFeeSpeed[] = []; for (const speed of BITCOIN_FEE_SPEED_ORDER) { const rate = getBitcoinFeeRate(rates, speed); if (!seen.has(rate)) { @@ -33,3 +41,24 @@ export function getUniqueBitcoinFeeSpeeds( } return result; } + +/** + * Resolve the effective sat/vB rate for the current selection. + * + * For `'custom'` the user-typed value wins (parsed and floored, valid only + * when ≥ 1). For preset tiers we read the loaded rates; returns `undefined` + * when rates haven't loaded (or a custom value isn't a usable rate), which + * callers should treat as "not ready" rather than a real rate. + */ +export function resolveBitcoinFeeRate( + speed: BitcoinFeeSpeed, + rates: BitcoinFeeRates | undefined, + customFeeRate: string, +): number | undefined { + if (speed === 'custom') { + const parsed = Math.floor(Number(customFeeRate)); + return Number.isFinite(parsed) && parsed >= 1 ? parsed : undefined; + } + if (!rates) return undefined; + return getBitcoinFeeRate(rates, speed); +} diff --git a/src/locales/ar.json b/src/locales/ar.json index fe6cd2ac..8af0e17d 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -1163,7 +1163,18 @@ "fastest": "~10 دقائق", "halfHour": "~30 دقيقة", "hour": "~ساعة", - "economy": "~يوم" + "economy": "~يوم", + "custom": "مخصّص" + }, + "fee": { + "loading": "جارٍ التحميل…", + "unavailable": "غير متاح", + "loadFailed": "تعذّر تحميل معدلات الرسوم.", + "retry": "إعادة المحاولة", + "orCustom": "أو أدخل معدلاً مخصّصاً أدناه.", + "loadingTiers": "جارٍ تحميل معدلات الرسوم…", + "customPlaceholder": "مثال: 5", + "customAriaLabel": "معدل رسوم مخصّص بوحدة sat/vB" }, "progress": { "building": "جارٍ بناء المعاملة…", @@ -1179,7 +1190,9 @@ "enterAmount": "أدخل مبلغاً.", "insufficient": "البيتكوين غير كافٍ لهذا المبلغ + رسوم الشبكة.", "waitingPrice": "في انتظار سعر BTC…", - "noneYet": "ليس لديك بيتكوين بعد." + "noneYet": "ليس لديك بيتكوين بعد.", + "feesNotLoadedYet": "لم يتم تحميل معدلات الرسوم بعد.", + "feeRateTooLow": "أدخل معدل رسوم لا يقل عن 1 sat/vB." }, "toast": { "failedTitle": "فشلت المعاملة" diff --git a/src/locales/en.json b/src/locales/en.json index 9c52c057..a50b8508 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1677,7 +1677,18 @@ "fastest": "~10 min", "halfHour": "~30 min", "hour": "~1 hour", - "economy": "~1 day" + "economy": "~1 day", + "custom": "Custom" + }, + "fee": { + "loading": "loading…", + "unavailable": "unavailable", + "loadFailed": "Couldn't load fee rates.", + "retry": "Retry", + "orCustom": "Or enter a custom rate below.", + "loadingTiers": "Loading fee rates…", + "customPlaceholder": "e.g. 5", + "customAriaLabel": "Custom fee rate in sat/vB" }, "progress": { "building": "Building transaction…", @@ -1690,6 +1701,8 @@ "enterRecipient": "Enter a Bitcoin address or sp1… silent payment address.", "noSpendable": "No spendable Bitcoin in this wallet.", "feesNotLoaded": "Fee rates not loaded.", + "feesNotLoadedYet": "Fee rates haven't loaded yet.", + "feeRateTooLow": "Enter a fee rate of at least 1 sat/vB.", "enterAmount": "Enter an amount.", "insufficient": "Not enough Bitcoin for this amount + network fee.", "waitingPrice": "Waiting for BTC price…", diff --git a/src/locales/es.json b/src/locales/es.json index 4f790bab..5bd72a1a 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1179,7 +1179,18 @@ "fastest": "~10 min", "halfHour": "~30 min", "hour": "~1 hora", - "economy": "~1 día" + "economy": "~1 día", + "custom": "Personalizada" + }, + "fee": { + "loading": "cargando…", + "unavailable": "no disponible", + "loadFailed": "No se pudieron cargar las tasas de comisión.", + "retry": "Reintentar", + "orCustom": "O introduce una tasa personalizada abajo.", + "loadingTiers": "Cargando las tasas de comisión…", + "customPlaceholder": "p. ej. 5", + "customAriaLabel": "Tasa de comisión personalizada en sat/vB" }, "progress": { "building": "Construyendo la transacción…", @@ -1195,7 +1206,9 @@ "enterAmount": "Introduce una cantidad.", "insufficient": "No hay Bitcoin suficiente para esta cantidad + la comisión de red.", "waitingPrice": "Esperando el precio del BTC…", - "noneYet": "Aún no tienes Bitcoin." + "noneYet": "Aún no tienes Bitcoin.", + "feesNotLoadedYet": "Las tasas de comisión aún no se han cargado.", + "feeRateTooLow": "Introduce una tasa de comisión de al menos 1 sat/vB." }, "scanError": { "title": "No se pudo leer ese código QR", diff --git a/src/locales/fa.json b/src/locales/fa.json index 64489e88..3a318ff7 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1179,7 +1179,18 @@ "fastest": "~۱۰ دقیقه", "halfHour": "~۳۰ دقیقه", "hour": "~۱ ساعت", - "economy": "~۱ روز" + "economy": "~۱ روز", + "custom": "سفارشی" + }, + "fee": { + "loading": "در حال بارگذاری…", + "unavailable": "در دسترس نیست", + "loadFailed": "بارگذاری نرخ‌های کارمزد ممکن نشد.", + "retry": "تلاش دوباره", + "orCustom": "یا یک نرخ سفارشی را در زیر وارد کنید.", + "loadingTiers": "در حال بارگذاری نرخ‌های کارمزد…", + "customPlaceholder": "مثلاً ۵", + "customAriaLabel": "نرخ کارمزد سفارشی به sat/vB" }, "progress": { "building": "در حال ساخت تراکنش…", @@ -1195,7 +1206,9 @@ "enterAmount": "یک مبلغ وارد کنید.", "insufficient": "بیت‌کوین کافی برای این مبلغ + کارمزد شبکه ندارید.", "waitingPrice": "در انتظار قیمت BTC…", - "noneYet": "هنوز بیت‌کوینی ندارید." + "noneYet": "هنوز بیت‌کوینی ندارید.", + "feesNotLoadedYet": "نرخ‌های کارمزد هنوز بارگذاری نشده‌اند.", + "feeRateTooLow": "یک نرخ کارمزد دست‌کم ۱ sat/vB وارد کنید." }, "toast": { "failedTitle": "تراکنش ناموفق بود" diff --git a/src/locales/fr.json b/src/locales/fr.json index bc6cbd66..4defb61a 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1602,7 +1602,18 @@ "fastest": "~10 min", "halfHour": "~30 min", "hour": "~1 heure", - "economy": "~1 jour" + "economy": "~1 jour", + "custom": "Personnalisé" + }, + "fee": { + "loading": "chargement…", + "unavailable": "indisponible", + "loadFailed": "Impossible de charger les taux de frais.", + "retry": "Réessayer", + "orCustom": "Ou saisissez un taux personnalisé ci-dessous.", + "loadingTiers": "Chargement des taux de frais…", + "customPlaceholder": "p. ex. 5", + "customAriaLabel": "Taux de frais personnalisé en sat/vB" }, "progress": { "building": "Construction de la transaction…", @@ -1618,7 +1629,9 @@ "enterAmount": "Saisissez un montant.", "insufficient": "Pas assez de Bitcoin pour ce montant + frais réseau.", "waitingPrice": "En attente du prix BTC…", - "noneYet": "Vous n'avez pas encore de Bitcoin." + "noneYet": "Vous n'avez pas encore de Bitcoin.", + "feesNotLoadedYet": "Les taux de frais ne sont pas encore chargés.", + "feeRateTooLow": "Saisissez un taux de frais d'au moins 1 sat/vB." }, "scanError": { "title": "Impossible de lire ce QR code", diff --git a/src/locales/hi.json b/src/locales/hi.json index 2453b770..24804419 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -1548,7 +1548,18 @@ "fastest": "~10 मिनट", "halfHour": "~30 मिनट", "hour": "~1 घंटा", - "economy": "~1 दिन" + "economy": "~1 दिन", + "custom": "कस्टम" + }, + "fee": { + "loading": "लोड हो रहा है…", + "unavailable": "अनुपलब्ध", + "loadFailed": "Fee rates लोड नहीं हो सकीं।", + "retry": "फिर कोशिश करें", + "orCustom": "या नीचे एक कस्टम रेट डालें।", + "loadingTiers": "Fee rates लोड हो रही हैं…", + "customPlaceholder": "उदा. 5", + "customAriaLabel": "sat/vB में कस्टम fee rate" }, "progress": { "building": "Transaction बन रहा है…", @@ -1564,7 +1575,9 @@ "enterAmount": "एक राशि दर्ज करें।", "insufficient": "इस राशि + नेटवर्क फ़ीस के लिए Bitcoin पर्याप्त नहीं।", "waitingPrice": "BTC दाम का इंतज़ार है…", - "noneYet": "आपके पास अभी कोई Bitcoin नहीं है।" + "noneYet": "आपके पास अभी कोई Bitcoin नहीं है।", + "feesNotLoadedYet": "Fee rates अभी तक लोड नहीं हुई हैं।", + "feeRateTooLow": "कम से कम 1 sat/vB का fee rate डालें।" }, "toast": { "failedTitle": "Transaction विफल" diff --git a/src/locales/id.json b/src/locales/id.json index eba6820d..0d2a4a5b 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -1548,7 +1548,18 @@ "fastest": "~10 menit", "halfHour": "~30 menit", "hour": "~1 jam", - "economy": "~1 hari" + "economy": "~1 hari", + "custom": "Khusus" + }, + "fee": { + "loading": "memuat…", + "unavailable": "tidak tersedia", + "loadFailed": "Tidak bisa memuat tarif biaya.", + "retry": "Coba lagi", + "orCustom": "Atau masukkan tarif khusus di bawah.", + "loadingTiers": "Memuat tarif biaya…", + "customPlaceholder": "misal 5", + "customAriaLabel": "Tarif biaya khusus dalam sat/vB" }, "progress": { "building": "Membangun transaksi…", @@ -1564,7 +1575,9 @@ "enterAmount": "Masukkan jumlah.", "insufficient": "Bitcoin tidak cukup untuk jumlah ini + biaya jaringan.", "waitingPrice": "Menunggu harga BTC…", - "noneYet": "Anda belum punya Bitcoin." + "noneYet": "Anda belum punya Bitcoin.", + "feesNotLoadedYet": "Tarif biaya belum dimuat.", + "feeRateTooLow": "Masukkan tarif biaya minimal 1 sat/vB." }, "scanError": { "title": "Tidak bisa membaca kode QR itu", diff --git a/src/locales/km.json b/src/locales/km.json index 31980fac..0173d67e 100644 --- a/src/locales/km.json +++ b/src/locales/km.json @@ -1179,7 +1179,18 @@ "fastest": "~១០ នាទី", "halfHour": "~៣០ នាទី", "hour": "~១ ម៉ោង", - "economy": "~១ ថ្ងៃ" + "economy": "~១ ថ្ងៃ", + "custom": "ផ្ទាល់ខ្លួន" + }, + "fee": { + "loading": "កំពុងផ្ទុក…", + "unavailable": "មិនអាចប្រើបាន", + "loadFailed": "មិនអាចផ្ទុកអត្រាថ្លៃបានទេ។", + "retry": "ព្យាយាមម្ដងទៀត", + "orCustom": "ឬបញ្ចូលអត្រាផ្ទាល់ខ្លួននៅខាងក្រោម។", + "loadingTiers": "កំពុងផ្ទុកអត្រាថ្លៃ…", + "customPlaceholder": "ឧ. 5", + "customAriaLabel": "អត្រាថ្លៃផ្ទាល់ខ្លួនជា sat/vB" }, "progress": { "building": "កំពុងបង្កើតប្រតិបត្តិការ…", @@ -1195,7 +1206,9 @@ "enterAmount": "បញ្ចូលចំនួន។", "insufficient": "មិនមាន Bitcoin គ្រប់គ្រាន់សម្រាប់ចំនួននេះ + ថ្លៃបណ្តាញ។", "waitingPrice": "កំពុងរង់ចាំតម្លៃ BTC…", - "noneYet": "អ្នកមិនទាន់មាន Bitcoin ទេ។" + "noneYet": "អ្នកមិនទាន់មាន Bitcoin ទេ។", + "feesNotLoadedYet": "អត្រាថ្លៃមិនទាន់បានផ្ទុកនៅឡើយទេ។", + "feeRateTooLow": "បញ្ចូលអត្រាថ្លៃយ៉ាងហោចណាស់ 1 sat/vB។" }, "scanError": { "title": "មិនអាចអានកូដ QR នេះបានទេ", diff --git a/src/locales/ps.json b/src/locales/ps.json index 054afd94..dae9bc8c 100644 --- a/src/locales/ps.json +++ b/src/locales/ps.json @@ -1179,7 +1179,18 @@ "fastest": "~۱۰ دقیقې", "halfHour": "~۳۰ دقیقې", "hour": "~۱ ساعت", - "economy": "~۱ ورځ" + "economy": "~۱ ورځ", + "custom": "دودیز" + }, + "fee": { + "loading": "لوډېږي…", + "unavailable": "د لاسرسي وړ نه دی", + "loadFailed": "د فیس نرخونه یې ونه لوډل شول.", + "retry": "بیا هڅه وکړئ", + "orCustom": "یا لاندې یو دودیز نرخ دننه کړئ.", + "loadingTiers": "د فیس نرخونه لوډېږي…", + "customPlaceholder": "بېلګه: 5", + "customAriaLabel": "دودیز د فیس نرخ په sat/vB کې" }, "progress": { "building": "د معاملې جوړول…", @@ -1195,7 +1206,9 @@ "enterAmount": "یوه اندازه دننه کړئ.", "insufficient": "د دې اندازې او د شبکې فیس لپاره کافي بټکوین نشته.", "waitingPrice": "د BTC قیمت ته انتظار…", - "noneYet": "تاسو لاهم هېڅ بټکوین نه لرئ." + "noneYet": "تاسو لاهم هېڅ بټکوین نه لرئ.", + "feesNotLoadedYet": "د فیس نرخونه لاهم نه دي لوډ شوي.", + "feeRateTooLow": "لږ تر لږه د 1 sat/vB د فیس نرخ دننه کړئ." }, "scanError": { "title": "هغه QR کوډ ونه لوستل شو", diff --git a/src/locales/pt.json b/src/locales/pt.json index 31988eab..c4f23e55 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -1612,7 +1612,18 @@ "fastest": "~10 min", "halfHour": "~30 min", "hour": "~1 hora", - "economy": "~1 dia" + "economy": "~1 dia", + "custom": "Personalizada" + }, + "fee": { + "loading": "carregando…", + "unavailable": "indisponível", + "loadFailed": "Não foi possível carregar as taxas.", + "retry": "Tentar novamente", + "orCustom": "Ou insira uma taxa personalizada abaixo.", + "loadingTiers": "Carregando taxas…", + "customPlaceholder": "ex. 5", + "customAriaLabel": "Taxa personalizada em sat/vB" }, "progress": { "building": "Construindo transação…", @@ -1625,6 +1636,8 @@ "enterRecipient": "Digite um endereço Bitcoin ou endereço de pagamento silencioso sp1….", "noSpendable": "Sem Bitcoin gastável nesta carteira.", "feesNotLoaded": "Taxas não carregadas.", + "feesNotLoadedYet": "As taxas ainda não foram carregadas.", + "feeRateTooLow": "Insira uma taxa de pelo menos 1 sat/vB.", "enterAmount": "Digite um valor.", "insufficient": "Bitcoin insuficiente para este valor + taxa de rede.", "waitingPrice": "Aguardando preço do BTC…", diff --git a/src/locales/ru.json b/src/locales/ru.json index 9cbfe6c8..9a0cc3ab 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1612,7 +1612,18 @@ "fastest": "~10 мин", "halfHour": "~30 мин", "hour": "~1 час", - "economy": "~1 день" + "economy": "~1 день", + "custom": "Другая" + }, + "fee": { + "loading": "загрузка…", + "unavailable": "недоступно", + "loadFailed": "Не удалось загрузить ставки комиссий.", + "retry": "Повторить", + "orCustom": "Или введите свою ставку ниже.", + "loadingTiers": "Загрузка ставок комиссий…", + "customPlaceholder": "напр. 5", + "customAriaLabel": "Своя ставка комиссии в sat/vB" }, "progress": { "building": "Создание транзакции…", @@ -1628,7 +1639,9 @@ "enterAmount": "Введите сумму.", "insufficient": "Недостаточно Bitcoin для этой суммы + комиссии сети.", "waitingPrice": "Ожидание цены BTC…", - "noneYet": "У вас пока нет Bitcoin." + "noneYet": "У вас пока нет Bitcoin.", + "feesNotLoadedYet": "Ставки комиссий ещё не загружены.", + "feeRateTooLow": "Введите ставку комиссии не менее 1 sat/vB." }, "scanError": { "title": "Не удалось прочитать этот QR-код", diff --git a/src/locales/sn.json b/src/locales/sn.json index ac4432fe..40edcf2a 100644 --- a/src/locales/sn.json +++ b/src/locales/sn.json @@ -1179,7 +1179,18 @@ "fastest": "~10 maminitsi", "halfHour": "~30 maminitsi", "hour": "~awa 1", - "economy": "~zuva 1" + "economy": "~zuva 1", + "custom": "Zvakasarudzwa" + }, + "fee": { + "loading": "kuloadwa…", + "unavailable": "hazviwanike", + "loadFailed": "Hatina kukwanisa kuloadha mitengo yemiripo.", + "retry": "Edza zvakare", + "orCustom": "Kana isa mutengo wakasarudzwa pazasi.", + "loadingTiers": "Kuloadha mitengo yemiripo…", + "customPlaceholder": "semuenzaniso 5", + "customAriaLabel": "Mutengo wemuripo wakasarudzwa mu-sat/vB" }, "progress": { "building": "Kuvaka chinoitwa…", @@ -1195,7 +1206,9 @@ "enterAmount": "Isa huwandu.", "insufficient": "Bitcoin haina kukwana pahuwandu uhwu + muripo wenetwork.", "waitingPrice": "Kumirira mutengo weBTC…", - "noneYet": "Hauna kana Bitcoin parizvino." + "noneYet": "Hauna kana Bitcoin parizvino.", + "feesNotLoadedYet": "Mitengo yemiripo haisati yaloadwa.", + "feeRateTooLow": "Isa mutengo wemuripo usingadarike 1 sat/vB." }, "toast": { "failedTitle": "Chinoitwa chakundikana" diff --git a/src/locales/sw.json b/src/locales/sw.json index 90d46148..d9953bb5 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -1507,7 +1507,18 @@ "fastest": "~dakika 10", "halfHour": "~dakika 30", "hour": "~saa 1", - "economy": "~siku 1" + "economy": "~siku 1", + "custom": "Maalum" + }, + "fee": { + "loading": "inapakia…", + "unavailable": "haipatikani", + "loadFailed": "Imeshindwa kupakia viwango vya ada.", + "retry": "Jaribu tena", + "orCustom": "Au weka kiwango maalum hapa chini.", + "loadingTiers": "Inapakia viwango vya ada…", + "customPlaceholder": "mf. 5", + "customAriaLabel": "Kiwango maalum cha ada katika sat/vB" }, "progress": { "building": "Inajenga muamala…", @@ -1523,7 +1534,9 @@ "enterAmount": "Weka kiasi.", "insufficient": "Hakuna Bitcoin ya kutosha kwa kiasi hiki + ada ya mtandao.", "waitingPrice": "Inasubiri bei ya BTC…", - "noneYet": "Bado huna Bitcoin yoyote." + "noneYet": "Bado huna Bitcoin yoyote.", + "feesNotLoadedYet": "Viwango vya ada bado havijapakiwa.", + "feeRateTooLow": "Weka kiwango cha ada cha angalau 1 sat/vB." }, "scanError": { "title": "Imeshindwa kusoma msimbo huo wa QR", diff --git a/src/locales/tr.json b/src/locales/tr.json index 7ddbfae9..cc256e0c 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1547,7 +1547,18 @@ "fastest": "~10 dk", "halfHour": "~30 dk", "hour": "~1 saat", - "economy": "~1 gün" + "economy": "~1 gün", + "custom": "Özel" + }, + "fee": { + "loading": "yükleniyor…", + "unavailable": "kullanılamıyor", + "loadFailed": "Ücret oranları yüklenemedi.", + "retry": "Yeniden dene", + "orCustom": "Ya da aşağıya özel bir oran girin.", + "loadingTiers": "Ücret oranları yükleniyor…", + "customPlaceholder": "örn. 5", + "customAriaLabel": "sat/vB cinsinden özel ücret oranı" }, "progress": { "building": "İşlem oluşturuluyor…", @@ -1563,7 +1574,9 @@ "enterAmount": "Bir tutar girin.", "insufficient": "Bu tutar + ağ ücreti için yeterli Bitcoin yok.", "waitingPrice": "BTC fiyatı bekleniyor…", - "noneYet": "Henüz Bitcoin'iniz yok." + "noneYet": "Henüz Bitcoin'iniz yok.", + "feesNotLoadedYet": "Ücret oranları henüz yüklenmedi.", + "feeRateTooLow": "En az 1 sat/vB ücret oranı girin." }, "toast": { "failedTitle": "İşlem başarısız" diff --git a/src/locales/zh-Hant.json b/src/locales/zh-Hant.json index 3b923b1a..7e28e9ae 100644 --- a/src/locales/zh-Hant.json +++ b/src/locales/zh-Hant.json @@ -1115,7 +1115,18 @@ "fastest": "~10 分鐘", "halfHour": "~30 分鐘", "hour": "~1 小時", - "economy": "~1 天" + "economy": "~1 天", + "custom": "自訂" + }, + "fee": { + "loading": "載入中…", + "unavailable": "無法使用", + "loadFailed": "無法載入費率。", + "retry": "重試", + "orCustom": "或在下方輸入自訂費率。", + "loadingTiers": "正在載入費率…", + "customPlaceholder": "例如 5", + "customAriaLabel": "自訂費率(sat/vB)" }, "progress": { "building": "正在構建交易…", @@ -1128,6 +1139,8 @@ "enterRecipient": "請輸入比特幣地址或 sp1… 靜默支付地址。", "noSpendable": "此錢包中沒有可花費的比特幣。", "feesNotLoaded": "費率未載入。", + "feesNotLoadedYet": "費率尚未載入。", + "feeRateTooLow": "請輸入至少 1 sat/vB 的費率。", "enterAmount": "請輸入金額。", "insufficient": "比特幣不足以支付該金額和網路費用。", "waitingPrice": "正在等待 BTC 價格…", diff --git a/src/locales/zh.json b/src/locales/zh.json index 66b674bb..57bd8b00 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1179,7 +1179,18 @@ "fastest": "~10 分钟", "halfHour": "~30 分钟", "hour": "~1 小时", - "economy": "~1 天" + "economy": "~1 天", + "custom": "自定义" + }, + "fee": { + "loading": "加载中…", + "unavailable": "不可用", + "loadFailed": "无法加载费率。", + "retry": "重试", + "orCustom": "或在下方输入自定义费率。", + "loadingTiers": "正在加载费率…", + "customPlaceholder": "例如 5", + "customAriaLabel": "自定义费率(sat/vB)" }, "progress": { "building": "正在构建交易…", @@ -1195,7 +1206,9 @@ "enterAmount": "请输入金额。", "insufficient": "比特币不足以支付该金额和网络费用。", "waitingPrice": "正在等待 BTC 价格…", - "noneYet": "你还没有任何比特币。" + "noneYet": "你还没有任何比特币。", + "feesNotLoadedYet": "费率尚未加载。", + "feeRateTooLow": "请输入至少 1 sat/vB 的费率。" }, "scanError": { "title": "无法读取该二维码",