diff --git a/projects/keepkey-vault/src/bun/reports.ts b/projects/keepkey-vault/src/bun/reports.ts index 7aa67b64..61b54356 100644 --- a/projects/keepkey-vault/src/bun/reports.ts +++ b/projects/keepkey-vault/src/bun/reports.ts @@ -12,7 +12,7 @@ */ import type { ReportData, ReportSection, ChainBalance } from '../shared/types' -import { getLatestDeviceSnapshot, getCachedPubkeys } from './db' +import { getLatestDeviceSnapshot, getCachedPubkeys, getSetting } from './db' import { getPioneer } from './pioneer' /** Section title prefixes — shared with tax-export.ts for reliable extraction. */ @@ -767,6 +767,9 @@ function sanitize(text: string): string { export async function reportToPdfBuffer(data: ReportData): Promise { const { PDFDocument, StandardFonts, rgb, degrees } = await import('pdf-lib') + const reportLocale = getSetting('number_locale') || 'en-US' + // Stored values are always USD — use USD currency label with user's number locale for separators + const reportCurrency = 'USD' console.log('[reports] Starting PDF generation...') @@ -897,7 +900,7 @@ export async function reportToPdfBuffer(data: ReportData): Promise { y -= 30 // ── Total Portfolio Value (big number) ── - const totalStr = `$${totalPortfolioUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + const totalStr = new Intl.NumberFormat(reportLocale, { style: 'currency', currency: reportCurrency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(totalPortfolioUsd) const totalLabel = 'Total Portfolio Value' const totalLabelW = font.widthOfTextAtSize(totalLabel, 11) const totalValW = bold.widthOfTextAtSize(totalStr, 28) @@ -958,7 +961,7 @@ export async function reportToPdfBuffer(data: ReportData): Promise { for (const slice of slices.slice(0, 12)) { const pct = ((slice.usd / totalPortfolioUsd) * 100).toFixed(1) - const usdStr = `$${slice.usd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + const usdStr = new Intl.NumberFormat(reportLocale, { style: 'currency', currency: reportCurrency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(slice.usd) // Color swatch page.drawRectangle({ diff --git a/projects/keepkey-vault/src/bun/swap-report.ts b/projects/keepkey-vault/src/bun/swap-report.ts index 819e9b62..b3e714d5 100644 --- a/projects/keepkey-vault/src/bun/swap-report.ts +++ b/projects/keepkey-vault/src/bun/swap-report.ts @@ -5,6 +5,7 @@ * CSV is plain-text, compatible with spreadsheet apps and tax tools. */ import type { SwapHistoryRecord } from '../shared/types' +import { getSetting } from './db' // ── CSV Export ──────────────────────────────────────────────────────── @@ -189,7 +190,8 @@ export async function generateSwapPdf(records: SwapHistoryRecord[]): Promise { + try { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency, + minimumFractionDigits: dec, + maximumFractionDigits: dec, + currencyDisplay: 'narrowSymbol', + }) + } catch { + return null + } + }, [locale, currency, dec]) + + const formatValue = (n: number) => { + const formatted = formatter ? formatter.format(n) : `${cfg.symbol}${n.toFixed(dec)}` + return `${prefix}${formatted}${suffix}` + } + if (!isFinite(value) || value <= 0) { - return {prefix}0.{'0'.repeat(decimals)}{suffix} + return {formatValue(0)} } return ( - {prefix} - - {suffix} + ) } diff --git a/projects/keepkey-vault/src/mainview/components/AssetPage.tsx b/projects/keepkey-vault/src/mainview/components/AssetPage.tsx index b9df6297..aa4fb0b2 100644 --- a/projects/keepkey-vault/src/mainview/components/AssetPage.tsx +++ b/projects/keepkey-vault/src/mainview/components/AssetPage.tsx @@ -8,7 +8,8 @@ import { CHAINS, BTC_SCRIPT_TYPES, btcAccountPath, isChainSupported } from "../. import type { ChainBalance, TokenBalance, TokenVisibilityStatus, AppSettings } from "../../shared/types" import { getAssetIcon, caipToIcon } from "../../shared/assetLookup" import { AnimatedUsd } from "./AnimatedUsd" -import { formatBalance, formatUsd } from "../lib/formatting" +import { formatBalance } from "../lib/formatting" +import { useFiat } from "../lib/fiat-context" import { ReceiveView } from "./ReceiveView" import { SendForm } from "./SendForm" @@ -48,6 +49,7 @@ interface AssetPageProps { export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPageProps) { const { t } = useTranslation("asset") + const { fmtCompact, symbol: fiatSymbol } = useFiat() const [view, setView] = useState("receive") const [selectedToken, setSelectedToken] = useState(null) const [address, setAddress] = useState(balance?.address || null) @@ -381,7 +383,7 @@ export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPage {tok.balanceUsd > 0 && ( - ${formatUsd(tok.balanceUsd)} + {fmtCompact(tok.balanceUsd)} )} @@ -476,7 +478,7 @@ export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPage {activeBalance.balance} {chain.symbol} {cleanBalanceUsd > 0 && ( - + )} )} {tokenTotalUsd > 0 && ( - ${formatUsd(tokenTotalUsd)} + {fmtCompact(tokenTotalUsd)} )} {isEvmChain && ( 0 && ( <> - {t("zeroValueTokens", { count: zeroValueTokens.length })} + {t("zeroValueTokens", { count: zeroValueTokens.length, zeroValue: fmtCompact(0) || `${fiatSymbol}0` })} {zeroValueTokens.map((tok) => renderTokenRow(tok))} diff --git a/projects/keepkey-vault/src/mainview/components/BtcXpubSelector.tsx b/projects/keepkey-vault/src/mainview/components/BtcXpubSelector.tsx index c61fcd5d..2bbebca7 100644 --- a/projects/keepkey-vault/src/mainview/components/BtcXpubSelector.tsx +++ b/projects/keepkey-vault/src/mainview/components/BtcXpubSelector.tsx @@ -2,7 +2,8 @@ import { Box, Flex, Text, Button } from "@chakra-ui/react" import { FaPlus } from "react-icons/fa" import { useTranslation } from "react-i18next" import { BTC_SCRIPT_TYPES } from "../../shared/chains" -import { formatBalance, formatUsd } from "../lib/formatting" +import { formatBalance } from "../lib/formatting" +import { useFiat } from "../lib/fiat-context" import type { BtcAccountSet, BtcScriptType } from "../../shared/types" interface BtcXpubSelectorProps { @@ -15,6 +16,7 @@ interface BtcXpubSelectorProps { export function BtcXpubSelector({ btcAccounts, onSelectXpub, onAddAccount, addingAccount }: BtcXpubSelectorProps) { const { accounts, selectedXpub } = btcAccounts const { t } = useTranslation("receive") + const { fmtCompact } = useFiat() if (accounts.length === 0) return null const selAcct = selectedXpub?.accountIndex ?? 0 @@ -95,7 +97,7 @@ export function BtcXpubSelector({ btcAccounts, onSelectXpub, onAddAccount, addin )} {xpubData && xpubData.balanceUsd > 0 && ( - ${formatUsd(xpubData.balanceUsd)} + {fmtCompact(xpubData.balanceUsd)} )} diff --git a/projects/keepkey-vault/src/mainview/components/EvmAddressSelector.tsx b/projects/keepkey-vault/src/mainview/components/EvmAddressSelector.tsx index 94ef69da..d73ef6de 100644 --- a/projects/keepkey-vault/src/mainview/components/EvmAddressSelector.tsx +++ b/projects/keepkey-vault/src/mainview/components/EvmAddressSelector.tsx @@ -1,6 +1,6 @@ import { Box, Flex, Text, Button } from "@chakra-ui/react" import { FaPlus, FaTimes } from "react-icons/fa" -import { formatUsd } from "../lib/formatting" +import { useFiat } from "../lib/fiat-context" import type { EvmAddressSet } from "../../shared/types" interface EvmAddressSelectorProps { @@ -12,6 +12,7 @@ interface EvmAddressSelectorProps { } export function EvmAddressSelector({ evmAddresses, onSelectIndex, onAddIndex, onRemoveIndex, adding }: EvmAddressSelectorProps) { + const { fmtCompact } = useFiat() const { addresses, selectedIndex } = evmAddresses // Don't render if only one address tracked @@ -56,7 +57,7 @@ export function EvmAddressSelector({ evmAddresses, onSelectIndex, onAddIndex, on {addr.balanceUsd > 0 && ( - ${formatUsd(addr.balanceUsd)} + {fmtCompact(addr.balanceUsd)} )} diff --git a/projects/keepkey-vault/src/mainview/components/ReportDialog.tsx b/projects/keepkey-vault/src/mainview/components/ReportDialog.tsx index 4e14c229..f062753f 100644 --- a/projects/keepkey-vault/src/mainview/components/ReportDialog.tsx +++ b/projects/keepkey-vault/src/mainview/components/ReportDialog.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useRef } from "react" import { Box, Flex, Text, Spinner, Image } from "@chakra-ui/react" import { rpcRequest, onRpcMessage } from "../lib/rpc" +import { useFiat } from "../lib/fiat-context" import { Z } from "../lib/z-index" import type { ReportMeta } from "../../shared/types" @@ -22,6 +23,7 @@ interface ReportDialogProps { } export function ReportDialog({ onClose }: ReportDialogProps) { + const { locale: fiatLocale, fmtCompact } = useFiat() const [generating, setGenerating] = useState(false) const [progress, setProgress] = useState<{ message: string; percent: number } | null>(null) const [reports, setReports] = useState([]) @@ -250,11 +252,11 @@ export function ReportDialog({ onClose }: ReportDialogProps) { Full Detail Report - {r.status === "error" ? "Failed" : `$${r.totalUsd.toFixed(2)}`} + {r.status === "error" ? "Failed" : fmtCompact(r.totalUsd)} - {new Date(r.createdAt).toLocaleString()} + {new Date(r.createdAt).toLocaleString(fiatLocale)} {r.error && ( {r.error} diff --git a/projects/keepkey-vault/src/mainview/components/SendForm.tsx b/projects/keepkey-vault/src/mainview/components/SendForm.tsx index e2f4d7b2..eca3347c 100644 --- a/projects/keepkey-vault/src/mainview/components/SendForm.tsx +++ b/projects/keepkey-vault/src/mainview/components/SendForm.tsx @@ -2,7 +2,8 @@ import { useState, useEffect, useCallback, useMemo, Fragment } from "react" import { useTranslation } from "react-i18next" import { Box, Flex, Text, VStack, Button, Input } from "@chakra-ui/react" import { rpcRequest } from "../lib/rpc" -import { formatBalance, formatUsd } from "../lib/formatting" +import { formatBalance } from "../lib/formatting" +import { useFiat } from "../lib/fiat-context" import { getAsset } from "../../shared/assetLookup" import { QrScannerOverlay } from "./QrScannerOverlay" import type { ChainDef } from "../../shared/chains" @@ -41,6 +42,7 @@ interface SendFormProps { export function SendForm({ chain, address, balance, token, onClearToken, xpubOverride, scriptTypeOverride, evmAddressIndex }: SendFormProps) { const { t } = useTranslation("send") + const { fmt, fmtCompact } = useFiat() const [recipient, setRecipient] = useState("") const [amount, setAmount] = useState("") const [usdAmount, setUsdAmount] = useState("") @@ -301,7 +303,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve {hasPrice && ( - ${formatUsd(parseFloat(displayBalance) * pricePerUnit)} + {fmtCompact(parseFloat(displayBalance) * pricePerUnit)} )} @@ -408,7 +410,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve {inputMode === 'crypto' ? ( - {amountUsdPreview !== null ? `$${formatUsd(amountUsdPreview)}` : '$0.00'} + {amountUsdPreview !== null ? (fmtCompact(amountUsdPreview) || fmt(0)) : fmt(0)} ) : ( @@ -421,7 +423,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve )} {pricePerUnit > 0 && ( - 1 {displaySymbol} = ${formatUsd(pricePerUnit)} + 1 {displaySymbol} = {fmtCompact(pricePerUnit)} )} )} @@ -492,7 +494,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve {isMax ? 'MAX' : amount} {displaySymbol} {!isMax && amountUsdPreview !== null && ( - ${formatUsd(amountUsdPreview)} + {fmtCompact(amountUsdPreview)} )} @@ -501,7 +503,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve {formatBalance(buildResult.fee)} {chain.symbol} {buildResult.feeUsd != null && buildResult.feeUsd > 0 && ( - ${formatUsd(buildResult.feeUsd)} + {fmtCompact(buildResult.feeUsd)} )} diff --git a/projects/keepkey-vault/src/mainview/components/StakingPanel.tsx b/projects/keepkey-vault/src/mainview/components/StakingPanel.tsx index 241966aa..c42749c1 100644 --- a/projects/keepkey-vault/src/mainview/components/StakingPanel.tsx +++ b/projects/keepkey-vault/src/mainview/components/StakingPanel.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next" import type { ChainDef } from "../../shared/chains" import type { BuildTxResult, BroadcastResult, StakingPosition } from "../../shared/types" import { rpcRequest } from "../lib/rpc" +import { useFiat } from "../lib/fiat-context" import { Z } from "../lib/z-index" interface StakingPanelProps { @@ -53,6 +54,7 @@ interface DelegateDialogProps { function DelegateDialog({ isOpen, onClose, chain, availableBalance, rewardAmount, rewardUsd, onSuccess, watchOnly }: DelegateDialogProps) { const { t } = useTranslation("staking") + const { fmtCompact } = useFiat() const [validatorAddress, setValidatorAddress] = useState("") const [amount, setAmount] = useState("") const [memo, setMemo] = useState(t('defaultDelegationMemo')) @@ -287,7 +289,7 @@ function DelegateDialog({ isOpen, onClose, chain, availableBalance, rewardAmount {rewardAmount && ( {rewardUsd && rewardUsd > 0 - ? t('rewardsAvailableWithUsd', { amount: rewardAmount, symbol: chain.symbol, usd: rewardUsd.toFixed(2) }) + ? t('rewardsAvailableWithUsd', { amount: rewardAmount, symbol: chain.symbol, fiatValue: fmtCompact(rewardUsd) }) : t('rewardsAvailable', { amount: rewardAmount, symbol: chain.symbol })} )} diff --git a/projects/keepkey-vault/src/mainview/components/SwapDialog.tsx b/projects/keepkey-vault/src/mainview/components/SwapDialog.tsx index 7e2c555c..b9a26ef6 100644 --- a/projects/keepkey-vault/src/mainview/components/SwapDialog.tsx +++ b/projects/keepkey-vault/src/mainview/components/SwapDialog.tsx @@ -10,7 +10,7 @@ import { Box, Flex, Text, VStack, Button, Input, Image, HStack } from "@chakra-u import CountUp from "react-countup" import { rpcRequest, onRpcMessage } from "../lib/rpc" import { formatBalance } from "../lib/formatting" -import { formatUsd } from "../lib/formatting" +import { useFiat } from "../lib/fiat-context" import { getAssetIcon } from "../../shared/assetLookup" import { CHAINS, getExplorerTxUrl } from "../../shared/chains" import type { ChainDef } from "../../shared/chains" @@ -240,7 +240,7 @@ interface AssetSelectorProps { function AssetSelector({ label, selected, assets, onSelect, balances, exclude, disabled, nativeOnly }: AssetSelectorProps) { const { t } = useTranslation("swap") - const fmtCompact = (v: number | string | null | undefined) => { const n = typeof v === 'string' ? parseFloat(v) : (v ?? 0); return !isFinite(n) || n === 0 ? '' : `$${formatUsd(n)}` } + const { fmtCompact } = useFiat() const [open, setOpen] = useState(false) const [search, setSearch] = useState("") const inputRef = useRef(null) @@ -445,8 +445,7 @@ interface SwapDialogProps { // ── Main SwapDialog ───────────────────────────────────────────────── export function SwapDialog({ open, onClose, chain, balance, address, resumeSwap }: SwapDialogProps) { const { t } = useTranslation("swap") - const fmtCompact = (v: number | string | null | undefined) => { const n = typeof v === 'string' ? parseFloat(v) : (v ?? 0); return !isFinite(n) || n === 0 ? '' : `$${formatUsd(n)}` } - const fiatSymbol = '$' + const { fmtCompact, symbol: fiatSymbol } = useFiat() // ── State ───────────────────────────────────────────────────────── const [phase, setPhase] = useState('input') diff --git a/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx b/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx index 8de11736..98672449 100644 --- a/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx +++ b/projects/keepkey-vault/src/mainview/components/ZcashPrivacyTab.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next" import { Box, Flex, Text, Button, Input, Spinner } from "@chakra-ui/react" import { FaShieldAlt, FaCopy, FaCheck, FaEnvelope, FaChevronDown, FaChevronUp } from "react-icons/fa" import { rpcRequest, onRpcMessage } from "../lib/rpc" +import { useFiat } from "../lib/fiat-context" import { generateQRSvg } from "../lib/qr" /** Validate Zcash recipient: unified (u1...), Sapling (zs1...), or transparent (t1.../t3...) */ @@ -45,6 +46,7 @@ function formatEta(seconds: number): string { export function ZcashPrivacyTab() { const { t } = useTranslation("asset") + const { locale: fiatLocale } = useFiat() // ── State ────────────────────────────────────────────────────────── const [status, setStatus] = useState("checking") @@ -497,7 +499,7 @@ export function ZcashPrivacyTab() { {scanState === "scanning" ? ( <> {t("scanning")} ) : ( - t("scanFromBlock", { block: KEEPKEY_RELEASE_BLOCK.toLocaleString() }) + t("scanFromBlock", { block: KEEPKEY_RELEASE_BLOCK.toLocaleString(fiatLocale) }) )} @@ -516,7 +518,7 @@ export function ZcashPrivacyTab() { )} {syncedTo && ( - {t("lastSynced", { height: syncedTo.toLocaleString() })} + {t("lastSynced", { height: syncedTo.toLocaleString(fiatLocale) })} )} @@ -778,7 +780,7 @@ export function ZcashPrivacyTab() { title="Click to use KeepKey release block" onClick={() => setScanFromHeight(String(KEEPKEY_RELEASE_BLOCK))} > - #{KEEPKEY_RELEASE_BLOCK.toLocaleString()} + #{KEEPKEY_RELEASE_BLOCK.toLocaleString(fiatLocale)} @@ -812,11 +814,11 @@ export function ZcashPrivacyTab() { {scanProgress ? ( - {scanProgress.scannedHeight.toLocaleString()} / {scanProgress.tipHeight.toLocaleString()} + {scanProgress.scannedHeight.toLocaleString(fiatLocale)} / {scanProgress.tipHeight.toLocaleString(fiatLocale)} {scanProgress.blocksPerSec > 0 && ( - {scanProgress.blocksPerSec.toLocaleString()} blk/s + {scanProgress.blocksPerSec.toLocaleString(fiatLocale)} blk/s )} @@ -1060,7 +1062,7 @@ export function ZcashPrivacyTab() { setScanFromHeight(String(tx.block_height)) }} > - #{tx.block_height.toLocaleString()} + #{tx.block_height.toLocaleString(fiatLocale)}