commit 1f8b707ecb19cb858d8618f62efa73e45a77cf91 parent 56e75d3fe6e23f71ab5c2ebd85d04d74a318f959 Author: Florian Dold <dold@taler.net> Date: Sun, 13 Sep 2026 17:15:33 +0200 wallet web UI: explain model and status messages to translators Document money direction, exchange services, payment scopes, transaction states and browser storage terms alongside their source messages. Diffstat:
20 files changed, 347 insertions(+), 239 deletions(-)
diff --git a/packages/wallet-webui/src/platform/durability.tsx b/packages/wallet-webui/src/platform/durability.tsx @@ -52,12 +52,12 @@ function rememberAcknowledgement(): void { function guidance(capability: PersistenceCapability | undefined): string { if (capability?.guidance === "chromium") { - return i18n.str`Chromium may grant protection after you install or bookmark this PWA and use it regularly. Install or bookmark it, then retry.`; + return /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Chromium may grant protection after you install or bookmark this PWA and use it regularly. Install or bookmark it, then retry.`; } if (capability?.guidance === "firefox") { - return i18n.str`Firefox asks for persistent-storage permission. Accept that prompt, then retry.`; + return /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Firefox asks for persistent-storage permission. Accept that prompt, then retry.`; } - return i18n.str`Install this PWA when your browser supports it, use it regularly, and retry. Export the database if you need a separate recovery copy.`; + return /* Translators: PWA means Progressive Web App: the browser wallet installed as an application. */ i18n.str`Install this PWA when your browser supports it, use it regularly, and retry. Export the database if you need a separate recovery copy.`; } export function useDurability(): DurabilityController { @@ -172,15 +172,21 @@ export function DurabilityProvider(props: { {props.children} {prompting && ( <ConfirmationDialog - title={i18n.str`Protect wallet data before continuing`} + title={ + /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Protect wallet data before continuing` + } description={ <> - <p>{i18n.str`The browser has not granted durable storage. Continuing may leave this wallet vulnerable to automatic storage eviction.`}</p> + <p>{ + /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`The browser has not granted durable storage. Continuing may leave this wallet vulnerable to automatic storage eviction.` + }</p> <p class="mt-3">{guidance(capability)}</p> </> } cancelLabel={i18n.str`Cancel`} - confirmLabel={i18n.str`Continue without protection`} + confirmLabel={ + /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Continue without protection` + } onCancel={() => finishPrompt(false)} onConfirm={() => finishPrompt(true)} > @@ -234,7 +240,9 @@ function ValueStorageWarning(props: { role="status" class="border-b border-error bg-errorContainer px-4 py-3 text-sm text-onErrorContainer" > - {i18n.str`This wallet contains value, but its browser storage is best-effort and may be removed automatically.`}{" "} + { + /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`This wallet contains value, but its browser storage is best-effort and may be removed automatically.` + }{" "} <a class="font-semibold underline" href="#/settings"> {i18n.str`Protect wallet data`} </a> diff --git a/packages/wallet-webui/src/routes/App.tsx b/packages/wallet-webui/src/routes/App.tsx @@ -409,10 +409,17 @@ function ProtocolCapture() { if (!error) return null; return ( <div class="fixed inset-x-4 top-4 z-50 mx-auto max-w-2xl"> - <ErrorCard title={i18n.str`Action could not be captured`} error={error}> + <ErrorCard + title={ + /* Translators: The wallet could not store an incoming Taler link for later review; no financial operation has been confirmed. */ i18n.str`Action could not be captured` + } + error={error} + > <div class="mt-4 flex flex-wrap gap-3"> <Button onClick={() => void capture()} disabled={working}> - {working ? i18n.str`Retrying…` : i18n.str`Retry capture`} + {working + ? i18n.str`Retrying…` + : /* Translators: Try saving the incoming Taler link to the local action inbox again. */ i18n.str`Retry capture`} </Button> <Button tone="secondary" onClick={() => setError(undefined)}> {i18n.str`Dismiss`} @@ -439,10 +446,10 @@ function WalletShell() { const label = demo ? i18n.str`Demo data` : connectionStatus.value.state === "connected" - ? i18n.str`Connected` + ? /* Translators: The UI is connected to its background wallet engine; this does not guarantee Internet connectivity. */ i18n.str`Connected` : connectionStatus.value.state === "connecting" - ? i18n.str`Connecting` - : i18n.str`Disconnected`; + ? /* Translators: The UI is connecting to its background wallet engine. */ i18n.str`Connecting` + : /* Translators: The UI has lost its connection to its background wallet engine. */ i18n.str`Disconnected`; return ( <Shell connectionLabel={label} @@ -799,9 +806,9 @@ function BalanceRoute() { } trendNotice={ selectedBalance?.scopeInfo.type === ScopeType.ExchangeLegacyKeys - ? i18n.str`Estimated history reconstructed from transactions tied to these superseded exchange keys; deleted activity is unavailable.` + ? /* Translators: Legacy funds belong to a previous cryptographic identity of an exchange, not merely an old wallet software version. */ i18n.str`Estimated history reconstructed from transactions tied to these superseded exchange keys; deleted activity is unavailable.` : selectedBalance?.scopeInfo.type === ScopeType.Exchange - ? i18n.str`Estimated history: a transaction involving more than one exchange is counted in full for each exchange, and deleted activity is unavailable.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Estimated history: a transaction involving more than one exchange is counted in full for each exchange, and deleted activity is unavailable.` : i18n.str`Estimated history reconstructed from retained completed transactions; deleted activity is unavailable.` } recentTransactions={recent.data?.transactions @@ -836,7 +843,7 @@ function BalanceRoute() { : undefined : selectedBalance ? localError( - i18n.str`Scoped transaction history is unavailable for this balance.`, + /* Translators: A payment scope groups funds by currency and exchange, merchant, or acceptance restrictions. */ i18n.str`Scoped transaction history is unavailable for this balance.`, ) : undefined } @@ -912,7 +919,7 @@ function normalizedDonauBaseUrl(value: string): string { ) ) { throw Error( - i18n.str`Use an HTTPS donation authority URL (HTTP is allowed only for a local test service).`, + /* Translators: The donation authority is the Donau service issuing donation receipts and statements, not the tax authority. */ i18n.str`Use an HTTPS donation authority URL (HTTP is allowed only for a local test service).`, ); } parsed.hash = ""; @@ -957,7 +964,7 @@ function DonauSettingsRoute() { setError( walletErrorHint( result.detail, - i18n.str`The donation authority could not be saved.`, + /* Translators: The donation authority is the Donau service issuing donation receipts and statements, not the tax authority. */ i18n.str`The donation authority could not be saved.`, ), ); if (Result.isError(result)) return; @@ -1361,7 +1368,7 @@ function DepositRoute() { ? localError(nextRecovery.message) : walletErrorHint( result.detail, - i18n.str`The wallet could not prepare this deposit.`, + /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`The wallet could not prepare this deposit.`, ), ); } else @@ -1374,7 +1381,10 @@ function DepositRoute() { } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Deposit preparation failed`), + errorFromException( + cause, + /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`Deposit preparation failed`, + ), ); } finally { setWorking(false); @@ -1414,7 +1424,7 @@ function DepositRoute() { ? localError(nextRecovery.message) : walletErrorHint( result.detail, - i18n.str`The wallet could not create this deposit.`, + /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`The wallet could not create this deposit.`, ), ); } else { @@ -1424,7 +1434,12 @@ function DepositRoute() { } } catch (cause) { if (!isProgressRequestCancelled(cause)) - setError(errorFromException(cause, i18n.str`Deposit creation failed`)); + setError( + errorFromException( + cause, + /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`Deposit creation failed`, + ), + ); } finally { setWorking(false); } @@ -1507,7 +1522,7 @@ function DepositRoute() { ), ...(response.kycExchanges?.length || response.kycSoftLimit ? { - kycWarning: i18n.str`This deposit may require identity verification at an exchange.`, + kycWarning: /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This deposit may require identity verification at an exchange.`, } : {}), } @@ -1715,7 +1730,9 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { values.scope ?? (values.scopeId ? scopeFromIdentity(values.scopeId) : undefined); if (!scope) - throw Error(i18n.str`Choose an available currency and payment scope.`); + throw Error( + /* Translators: A payment scope groups funds by currency and exchange, merchant, or acceptance restrictions. */ i18n.str`Choose an available currency and payment scope.`, + ); const amount = amountFromForm(values); const explicitExpiration = values.expiration.kind === "default" @@ -1753,12 +1770,11 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { ...(explicitExpiration ? { expiration: explicitExpiration } : {}), expirationLabel: explicitExpiration ? expirationLabel(explicitExpiration) - : // Translators: The peer-transfer expiration selected by - // wallet-core when the user does not choose one explicitly. + : // Translators: The wallet chooses this peer-payment expiry when the user does not specify one. i18n.str`Default`, amountRaw: amount, amountEffective: amount, - exchange: i18n.str`No suitable exchange`, + exchange: /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`No suitable exchange`, scope, insufficient: recovery?.message ?? @@ -1781,7 +1797,7 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { } if (!result.value.peerPushDebitQuote) { throw Error( - i18n.str`This wallet-core version cannot bind the reviewed transfer to confirmation. Update the wallet before sending.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`This wallet-core version cannot bind the reviewed transfer to confirmation. Update the wallet before sending.`, ); } setPrepared({ @@ -1827,7 +1843,10 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Peer payment preparation failed`), + errorFromException( + cause, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`Peer payment preparation failed`, + ), ); } finally { setWorking(false); @@ -1868,7 +1887,7 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { setError( walletCoreError( result.detail, - i18n.str`The available coins, fees, or exchange changed. Review the transfer again before sending.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The available coins, fees, or exchange changed. Review the transfer again before sending.`, ), ); } else if (Result.isError(result)) { @@ -1895,7 +1914,9 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { } } else { if (!prepared.expiration) { - throw Error(i18n.str`The request expiration is missing.`); + throw Error( + /* Translators: The peer-pull payment request has no configured expiration date. */ i18n.str`The request expiration is missing.`, + ); } const result = await progressRequest.callAndInvalidate( WalletApiOperation.InitiatePeerPullCredit, @@ -1930,7 +1951,10 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Peer payment creation failed`), + errorFromException( + cause, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`Peer payment creation failed`, + ), ); } finally { setWorking(false); @@ -1979,7 +2003,7 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { : exchanges.error ? errorFromException( exchanges.error, - i18n.str`Exchanges could not be loaded.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Exchanges could not be loaded.`, ) : undefined) } @@ -1987,7 +2011,7 @@ function PeerCreateRoute(props: { mode: "send" | "request" }) { recovery?.message ?? gate?.reason ?? (trustCheckPending - ? i18n.str`Checking this exchange's terms and signing keys…` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Checking this exchange's terms and signing keys…` : undefined) } blockedActionLabel={recovery?.actionLabel ?? gate?.label} @@ -2071,7 +2095,7 @@ function PeerShareRoute() { transaction && !terminalWithoutUri && transaction.error ? transactionErrorPresentation( transaction, - i18n.str`The exchange could not prepare the peer share link yet.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`The exchange could not prepare the peer share link yet.`, ) : undefined; const recovery = exchangeKeyRecoveryFromError(transaction?.error); @@ -2084,16 +2108,18 @@ function PeerShareRoute() { : query.error ? errorFromException( query.error, - i18n.str`The peer share link could not be loaded.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`The peer share link could not be loaded.`, ) : transaction && transaction.type !== TransactionType.PeerPushDebit && transaction.type !== TransactionType.PeerPullCredit ? localError( - i18n.str`This transaction does not have a peer share link.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`This transaction does not have a peer share link.`, ) : terminalWithoutUri - ? localError(i18n.str`This peer link is no longer available.`) + ? localError( + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`This peer link is no longer available.`, + ) : transactionProblem; const loading = query.isLoading || (!uri && !error); const [delayed, setDelayed] = useState(false); @@ -2117,7 +2143,7 @@ function PeerShareRoute() { setActionError( walletCoreError( result.detail, - i18n.str`The wallet could not retry preparing the peer share link.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`The wallet could not retry preparing the peer share link.`, ), ); } @@ -2125,7 +2151,7 @@ function PeerShareRoute() { setActionError( errorFromException( cause, - i18n.str`Retrying the peer share link failed.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`Retrying the peer share link failed.`, ), ); } finally { @@ -2203,7 +2229,7 @@ function PeerReceiveRoute() { const entry = await platform.actionInbox.get(actionId); if (!entry) throw Error( - i18n.str`This peer payment expired or was already completed.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`This peer payment expired or was already completed.`, ); const parsed = TalerUris.parse(entry.uri); if ( @@ -2211,7 +2237,9 @@ function PeerReceiveRoute() { (parsed.value.type !== TalerUriAction.PayPush && parsed.value.type !== TalerUriAction.PayPull) ) - throw Error(i18n.str`This is not a supported peer payment link.`); + throw Error( + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`This is not a supported peer payment link.`, + ); const kind = parsed.value.type === TalerUriAction.PayPush ? "receive" : "pay"; if (active) setKind(kind); @@ -2232,7 +2260,7 @@ function PeerReceiveRoute() { setError( walletErrorHint( result.detail, - i18n.str`The wallet could not prepare this peer payment.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`The wallet could not prepare this peer payment.`, ), ); return; @@ -2274,11 +2302,11 @@ function PeerReceiveRoute() { outcome.kind === "wallet" ? walletErrorHint( outcome.detail, - i18n.str`The peer payment could not be cancelled.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`The peer payment could not be cancelled.`, ) : errorFromException( outcome.cause, - i18n.str`Peer payment cancellation failed`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`Peer payment cancellation failed`, ), ); return; @@ -2320,7 +2348,7 @@ function PeerReceiveRoute() { setError( walletErrorHint( result.detail, - i18n.str`The wallet could not confirm this peer payment.`, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`The wallet could not confirm this peer payment.`, ), ); } @@ -2331,7 +2359,10 @@ function PeerReceiveRoute() { } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Peer payment confirmation failed`), + errorFromException( + cause, + /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`Peer payment confirmation failed`, + ), ); } finally { setWorking(false); @@ -2508,14 +2539,16 @@ function WithdrawExchangeRoute() { const entry = await platform.actionInbox.get(actionId); if (!entry) throw Error( - i18n.str`This exchange withdrawal expired or was already completed.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This exchange withdrawal expired or was already completed.`, ); const parsed = TalerUris.parse(entry.uri); if ( Result.isError(parsed) || parsed.value.type !== TalerUriAction.WithdrawExchange ) - throw Error(i18n.str`This is not an exchange withdrawal link.`); + throw Error( + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This is not an exchange withdrawal link.`, + ); const prepared = await progressRequest.callForResult( WalletApiOperation.PrepareWithdrawExchange, { talerUri: entry.uri }, @@ -2525,7 +2558,7 @@ function WithdrawExchangeRoute() { setError( walletErrorHint( prepared.detail, - i18n.str`Wallet-core could not prepare this exchange withdrawal.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not prepare this exchange withdrawal.`, ), ); return; @@ -2539,7 +2572,7 @@ function WithdrawExchangeRoute() { setError( walletErrorHint( exchange.detail, - i18n.str`Wallet-core prepared the withdrawal but did not return the exchange details.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core prepared the withdrawal but did not return the exchange details.`, ), ); return; @@ -2563,7 +2596,7 @@ function WithdrawExchangeRoute() { setError( errorFromException( cause, - i18n.str`The exchange withdrawal could not be prepared.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange withdrawal could not be prepared.`, ), ); } @@ -2722,7 +2755,7 @@ function WithdrawalRoute( setError( walletCoreError( result.detail, - i18n.str`The exchange could not prepare this withdrawal.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange could not prepare this withdrawal.`, ), ); } else { @@ -2738,7 +2771,10 @@ function WithdrawalRoute( } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Withdrawal preparation failed`), + errorFromException( + cause, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdrawal preparation failed`, + ), ); } finally { setPreparing(false); @@ -2767,7 +2803,7 @@ function WithdrawalRoute( setError( walletErrorHint( result.detail, - i18n.str`The exchange could not create this withdrawal.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange could not create this withdrawal.`, ), ); } else { @@ -2778,7 +2814,10 @@ function WithdrawalRoute( } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Withdrawal creation failed`), + errorFromException( + cause, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdrawal creation failed`, + ), ); } finally { setAccepting(false); @@ -2797,13 +2836,12 @@ function WithdrawalRoute( ? { title: current.state === "Completed" - ? i18n.str`Withdrawal completed` - : // Translators: Outcome heading for a withdrawal. The - // placeholder is an already translated transaction status. + ? /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdrawal completed` + : // Translators: Outcome heading; %1$s is an already translated withdrawal status. i18n.str`Withdrawal: ${current.state}`, detail: current.state === "Completed" - ? i18n.str`${current.amount} is now reflected in your wallet balance.` + ? /* Translators: %1$s is a formatted amount, including its currency. */ i18n.str`${current.amount} is now reflected in your wallet balance.` : current.statusDetail, success: current.tone === "success", } @@ -2906,7 +2944,9 @@ function WithdrawalRoute( preparing={preparing} demo={demo} title={ - directedByExchangeUri ? i18n.str`Withdraw from exchange` : undefined + directedByExchangeUri + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Withdraw from exchange` + : undefined } subtitle={ directedByExchangeUri @@ -2934,7 +2974,7 @@ function WithdrawalRoute( : query.error ? errorFromException( query.error, - i18n.str`Withdrawal exchanges could not be loaded.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Withdrawal exchanges could not be loaded.`, ) : allExchanges.error ? errorFromException( @@ -3125,7 +3165,7 @@ function ActionRoute() { setActionError( walletCoreError( result.detail, - i18n.str`Wallet-core could not prepare this payment.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not prepare this payment.`, ), ); setCanRetry(true); @@ -3143,7 +3183,7 @@ function ActionRoute() { setActionError( walletCoreError( checked.detail, - i18n.str`Wallet-core could not load this payment template.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not load this payment template.`, ), ); setCanRetry(true); @@ -3167,7 +3207,7 @@ function ActionRoute() { setActionError( walletCoreError( result.detail, - i18n.str`Wallet-core could not prepare this payment template.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not prepare this payment template.`, ), ); setCanRetry(true); @@ -3192,7 +3232,7 @@ function ActionRoute() { setActionError( walletCoreError( result.detail, - i18n.str`Wallet-core could not add this exchange.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not add this exchange.`, ), ); setCanRetry(true); @@ -3213,7 +3253,7 @@ function ActionRoute() { setActionError( walletCoreError( result.detail, - i18n.str`Wallet-core could not find the matching withdrawal.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not find the matching withdrawal.`, ), ); setCanRetry(true); @@ -3242,7 +3282,7 @@ function ActionRoute() { setActionError( walletCoreError( result.detail, - i18n.str`Wallet-core could not prepare this refund.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not prepare this refund.`, ), ); setCanRetry(true); @@ -3280,7 +3320,7 @@ function ActionRoute() { setActionError( walletCoreError( result.detail, - i18n.str`Wallet-core could not apply the developer experiment.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not apply the developer experiment.`, ), ); setCanRetry(true); @@ -3329,7 +3369,7 @@ function ActionRoute() { parsed && Result.isOk(parsed) ? parsed.value.type : parsedPayto && Result.isOk(parsedPayto) - ? i18n.str`Bank-account deposit` + ? /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`Bank-account deposit` : undefined } source={entry?.source} @@ -3338,7 +3378,7 @@ function ActionRoute() { detail ?? (parsed && Result.isOk(parsed) ? parsed.value.type === TalerUriAction.AddExchange - ? i18n.str`Add ${parsed.value.exchangeBaseUrl} as a wallet exchange.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Add ${parsed.value.exchangeBaseUrl} as a wallet exchange.` : parsed.value.type === TalerUriAction.DevExperiment ? entry?.source === "qr" ? i18n.str`This QR code requests the developer experiment “${parsed.value.devExperimentId}”. Experiments can deliberately alter wallet behavior or data. Only continue if you trust its source.` @@ -3353,7 +3393,7 @@ function ActionRoute() { continueLabel={ parsed && Result.isOk(parsed) ? parsed.value.type === TalerUriAction.WithdrawalTransferResult - ? i18n.str`View withdrawal` + ? /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`View withdrawal` : parsed.value.type === TalerUriAction.DevExperiment ? version.data?.devMode === true ? i18n.str`Apply experiment` @@ -3411,7 +3451,7 @@ function IntegratedWithdrawalRoute() { setPreparation(undefined); setError( localError( - i18n.str`This withdrawal action expired or was already completed.`, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`This withdrawal action expired or was already completed.`, ), ); setLoading(false); @@ -3427,7 +3467,9 @@ function IntegratedWithdrawalRoute() { if (isActive()) { setPreparation(undefined); setError( - localError(i18n.str`This action is not a valid bank withdrawal.`), + localError( + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`This action is not a valid bank withdrawal.`, + ), ); setLoading(false); } @@ -3444,7 +3486,7 @@ function IntegratedWithdrawalRoute() { setError( walletErrorHint( result.detail, - i18n.str`The bank withdrawal could not be prepared.`, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`The bank withdrawal could not be prepared.`, ), ); } else { @@ -3516,7 +3558,7 @@ function IntegratedWithdrawalRoute() { setError( walletErrorHint( result.detail, - i18n.str`The bank withdrawal status could not be refreshed.`, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`The bank withdrawal status could not be refreshed.`, ), ); } else { @@ -3569,11 +3611,11 @@ function IntegratedWithdrawalRoute() { outcome.kind === "wallet" ? walletErrorHint( outcome.detail, - i18n.str`The bank withdrawal could not be cancelled.`, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`The bank withdrawal could not be cancelled.`, ) : errorFromException( outcome.cause, - i18n.str`Bank withdrawal cancellation failed`, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Bank withdrawal cancellation failed`, ), ); return; @@ -3661,7 +3703,7 @@ function IntegratedWithdrawalRoute() { setError( walletErrorHint( result.detail, - i18n.str`The withdrawal could not be confirmed.`, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`The withdrawal could not be confirmed.`, ), ); } else { @@ -3681,7 +3723,10 @@ function IntegratedWithdrawalRoute() { } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Withdrawal confirmation failed`), + errorFromException( + cause, + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdrawal confirmation failed`, + ), ); } finally { setAccepting(false); @@ -3715,7 +3760,7 @@ function IntegratedWithdrawalRoute() { exchange={ prepared?.provider.exchangeBaseUrl ?? info.defaultExchangeBaseUrl ?? - i18n.str`Selected exchange` + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Selected exchange` } status="selected" hasConfirmationUrl={Boolean(confirmationUrl)} @@ -3735,7 +3780,7 @@ function IntegratedWithdrawalRoute() { amount={info.amount ?? i18n.str`Amount selected at cash acceptor`} exchange={ info.defaultExchangeBaseUrl ?? - i18n.str`Exchange selected at cash acceptor` + /* Translators: A cash acceptor is a facility where the user pays physical cash to fund a digital-cash withdrawal into this wallet. */ i18n.str`Exchange selected at cash acceptor` } status={info.status} hasConfirmationUrl={Boolean(confirmationUrl)} @@ -3821,10 +3866,12 @@ function IntegratedWithdrawalRoute() { preparing={cashAcceptor ? accepting : preparing} cancelling={cancelling} demo={false} - title={i18n.str`Withdraw from your bank`} + title={ + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdraw from your bank` + } subtitle={ cashAcceptor - ? i18n.str`Prepare the wallet before depositing cash` + ? /* Translators: A cash acceptor is a facility where the user pays physical cash to fund a digital-cash withdrawal into this wallet. */ i18n.str`Prepare the wallet before depositing cash` : i18n.str`Choose how much digital cash to receive` } initialProviderUrl={setup.initialProviderUrl} @@ -3838,7 +3885,7 @@ function IntegratedWithdrawalRoute() { error ?? (setup.providers.length === 0 ? localError( - i18n.str`No compatible exchange is available for this bank withdrawal.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`No compatible exchange is available for this bank withdrawal.`, ) : undefined) } @@ -4289,7 +4336,7 @@ function PaymentRoute() { const statusTransaction = repurchaseTransactionId ? (replayPayment ?? transaction) : transaction; - const statusFallback = i18n.str`Payment status: ${transactionStateView(statusTransaction).label}`; + const statusFallback = /* Translators: %1$s is a translated transaction-status label, which may be a phrase rather than an adjective. */ i18n.str`Payment status: ${transactionStateView(statusTransaction).label}`; const repurchaseError = repurchaseQuery.error && repurchaseTransactionId ? errorFromException( @@ -4459,7 +4506,7 @@ function PaymentDialog(props: { setError( walletCoreError( result.detail, - i18n.str`Wallet-core could not confirm this payment.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not confirm this payment.`, ), ); setState("error"); @@ -4618,7 +4665,7 @@ function PaymentDialog(props: { setError( errorFromException( cause, - i18n.str`Cancelling the payment handoff failed.`, + /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`Cancelling the payment handoff failed.`, ), ); }); @@ -4655,7 +4702,12 @@ function PaymentDialog(props: { if (wantsCurrentWallet()) { reclaimAfterward = true; } else if (!isProgressRequestCancelled(cause)) { - setError(errorFromException(cause, i18n.str`Payment handoff failed`)); + setError( + errorFromException( + cause, + /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`Payment handoff failed`, + ), + ); setState("ready"); } } finally { @@ -5048,7 +5100,7 @@ function TransactionDetailRoute() { setError( walletErrorHint( result.detail, - i18n.str`Wallet-core rejected this transaction action.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core rejected this transaction action.`, ), ); } else if (action === "delete") { @@ -5057,7 +5109,7 @@ function TransactionDetailRoute() { setMessage( action === "refund" ? i18n.str`The wallet is collecting the available refund.` - : i18n.str`Transaction action accepted.`, + : /* Translators: The wallet accepted the requested control action; the financial transaction may still be processing. */ i18n.str`Transaction action accepted.`, ); } } catch (cause) { @@ -5074,8 +5126,7 @@ function TransactionDetailRoute() { title: current.state === "Completed" ? i18n.str`Withdrawal completed` - : // Translators: Outcome heading for a withdrawal. The - // placeholder is an already translated transaction status. + : // Translators: Outcome heading; %1$s is an already translated withdrawal status. i18n.str`Withdrawal: ${current.state}`, detail: current.state === "Completed" @@ -5344,7 +5395,7 @@ function SettingsRoute() { const persisted = await durability.requestProtection(); if (persisted !== "granted") { throw Error( - i18n.str`Durable browser storage was not granted. Migration was not started.`, + /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Durable browser storage was not granted. Migration was not started.`, ); } } @@ -5354,8 +5405,8 @@ function SettingsRoute() { await mutate((key) => isWalletKey(key)); setMessage( direction === "forward" - ? i18n.str`SQLite migration completed and wallet-core reopened.` - : i18n.str`IndexedDB rollback completed and wallet-core reopened.`, + ? /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`SQLite migration completed and wallet-core reopened.` + : /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`IndexedDB rollback completed and wallet-core reopened.`, ); } catch (error) { setStorageError( @@ -5524,7 +5575,7 @@ function SettingsRoute() { setImportError( walletErrorHint( result.detail, - i18n.str`Wallet-core rejected the database import.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core rejected the database import.`, ), ); setImportStage("failure"); @@ -5555,7 +5606,7 @@ function SettingsRoute() { setImportError( walletErrorHint( result.detail, - i18n.str`Wallet-core rejected the recovery backup.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core rejected the recovery backup.`, ), ); setImportStage("failure"); @@ -5595,7 +5646,7 @@ function SettingsRoute() { setClearWalletError( walletErrorHint( result.detail, - i18n.str`Wallet-core could not clear the database.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core could not clear the database.`, ), ); return; @@ -5634,7 +5685,7 @@ function SettingsRoute() { setDeveloperError( walletErrorHint( result.detail, - i18n.str`Wallet-core developer mode could not be changed.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core developer mode could not be changed.`, ), ); return; @@ -5747,7 +5798,7 @@ function SettingsRoute() { (version.error ? errorFromException( version.error, - i18n.str`Wallet-core version information could not be loaded.`, + /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core version information could not be loaded.`, ) : undefined) } @@ -5769,11 +5820,11 @@ function SettingsRoute() { void durability.requestProtection().then((result) => { if (result === "granted") { setPersistenceMessage( - i18n.str`Wallet data is protected from automatic deletion.`, + /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Wallet data is protected from automatic deletion.`, ); } else { setPersistenceMessage( - `${i18n.str`Durable browser storage was not granted.`} ${durabilityGuidance(durability.capability)}`, + `${/* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Durable browser storage was not granted.`} ${durabilityGuidance(durability.capability)}`, ); } }) @@ -5833,7 +5884,9 @@ function StorageRecoveryRoute() { setRuntime(result); navigate("/settings", { replace: true }); } catch { - setError(i18n.str`Generation recovery failed`); + setError( + /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Generation recovery failed`, + ); try { setRuntime(await storage.getStatus()); } catch { @@ -5869,7 +5922,7 @@ function StorageRecoveryRoute() { <RecoveryScreen reason={ metadata?.migration.error ?? - i18n.str`The selected storage generation failed its post-activation checks.` + /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`The selected storage generation failed its post-activation checks.` } backend={ metadata?.migration.recoveryKind === "control-metadata-lost" diff --git a/packages/wallet-webui/src/routes/ManagementRoutes.tsx b/packages/wallet-webui/src/routes/ManagementRoutes.tsx @@ -102,7 +102,7 @@ export function ExchangesRoute() { setError( errorHint( result.detail, - i18n.str`The wallet could not add this exchange.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The wallet could not add this exchange.`, ), ); else @@ -110,7 +110,10 @@ export function ExchangesRoute() { } catch (cause) { if (!isProgressRequestCancelled(cause)) setError( - errorFromException(cause, i18n.str`Adding the exchange failed`), + errorFromException( + cause, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Adding the exchange failed`, + ), ); } finally { setWorking(false); @@ -155,7 +158,7 @@ export function ExchangeDetailRoute() { try { exchangeUrl = exchangeUrlFromRoute(params?.exchangeId ?? ""); } catch { - routeError = i18n.str`Invalid exchange route`; + routeError = /* Translators: Route means a navigation URL within the wallet interface, not a bank-transfer path. */ i18n.str`Invalid exchange route`; } const list = useWalletQuery(connection, WalletApiOperation.ListExchanges, {}); const details = useWalletQuery( @@ -243,7 +246,7 @@ export function ExchangeDetailRoute() { setError( errorHint( result.detail, - i18n.str`The exchange update could not be started.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange update could not be started.`, ), ); else setShowUpdateNotice(true); @@ -264,7 +267,10 @@ export function ExchangeDetailRoute() { ), ); else if (returnTo) navigate(returnTo); - else setMessage(i18n.str`Exchange key change confirmed.`); + else + setMessage( + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Exchange key change confirmed.`, + ); } else if (operation === "purge-legacy" && entry.masterPub) { const result = await callMutation( WalletApiOperation.PurgeExchangeLegacyKeys, @@ -278,14 +284,22 @@ export function ExchangeDetailRoute() { setError( errorHint( result.detail, - i18n.str`The legacy balances could not be purged. Refresh the exchange details and review them again.`, + /* Translators: Legacy funds belong to a previous cryptographic identity of an exchange, not merely an old wallet software version. */ i18n.str`The legacy balances could not be purged. Refresh the exchange details and review them again.`, ), ); else if (returnTo) navigate(returnTo); - else setMessage(i18n.str`Legacy exchange balances purged.`); + else + setMessage( + /* Translators: Legacy funds belong to a previous cryptographic identity of an exchange, not merely an old wallet software version. */ i18n.str`Legacy exchange balances purged.`, + ); } } catch (cause) { - setError(errorFromException(cause, i18n.str`Exchange action failed`)); + setError( + errorFromException( + cause, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Exchange action failed`, + ), + ); } finally { setWorking(false); } @@ -304,7 +318,7 @@ export function ExchangeDetailRoute() { setError( errorHint( resources.detail, - i18n.str`The wallet could not check exchange resources.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The wallet could not check exchange resources.`, ), ); } else { @@ -337,7 +351,7 @@ export function ExchangeDetailRoute() { setError( errorHint( result.detail, - i18n.str`The exchange could not be removed.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange could not be removed.`, ), ); } else { @@ -360,7 +374,7 @@ export function ExchangeDetailRoute() { ? details.error ? errorFromException( details.error, - i18n.str`Exchange details could not be loaded.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Exchange details could not be loaded.`, ) : undefined : undefined; @@ -384,7 +398,7 @@ export function ExchangeDetailRoute() { message={ message ?? (showUpdateNotice && updateInProgress - ? i18n.str`Exchange update started.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Exchange update started.` : undefined) } deleteMode={deleteMode} @@ -473,7 +487,7 @@ export function ExchangeTermsRoute() { setLoadError( errorFromException( cause, - i18n.str`Exchange terms could not be loaded.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Exchange terms could not be loaded.`, ), ); }, diff --git a/packages/wallet-webui/src/routes/action-model.ts b/packages/wallet-webui/src/routes/action-model.ts @@ -34,14 +34,14 @@ export function withdrawalTransferResultHint( status: "success" | "aborted" | undefined, ): string { if (status === "success") { - return i18n.str`Your bank reports that the transfer succeeded. The wallet will verify the withdrawal independently.`; + return /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Your bank reports that the transfer succeeded. The wallet will verify the withdrawal independently.`; } if (status === "aborted") { return i18n.str`Your bank reports that the transfer was aborted. This is a hint only; the wallet's own transaction state remains authoritative.`; } - return i18n.str`Open the matching withdrawal to see the wallet's verified status.`; + return /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Open the matching withdrawal to see the wallet's verified status.`; } export function restoreActionGateReason(): string { - return i18n.str`Wallet restore links require provider-backed backup synchronization and merging into the current wallet. This build does not support that wallet-core operation, and database import is not a safe substitute.`; + return /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet restore links require provider-backed backup synchronization and merging into the current wallet. This build does not support that wallet-core operation, and database import is not a safe substitute.`; } diff --git a/packages/wallet-webui/src/routes/balance-model.ts b/packages/wallet-webui/src/routes/balance-model.ts @@ -40,15 +40,15 @@ function refreshRiskReason(reason: CashExpirationRisk["reason"]): string { case "pending": return i18n.str`Renewal is pending.`; case "connectivity": - return i18n.str`The exchange could not be reached.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange could not be reached.`; case "exchange-error": - return i18n.str`The exchange reported an error during renewal.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange reported an error during renewal.`; case "no-replacement": - return i18n.str`The exchange currently offers no suitable replacement coins.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange currently offers no suitable replacement coins.`; case "checking": return i18n.str`Checking whether these coins can be renewed.`; case "invalid-lifetime": - return i18n.str`The exchange provided invalid coin validity dates.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange provided invalid coin validity dates.`; } } @@ -59,18 +59,18 @@ function refreshBoundReason(reason: string): string { case "expired-funds": return i18n.str`Some funds have expired.`; case "stale-key-information": - return i18n.str`Waiting for fresh exchange key information.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Waiting for fresh exchange key information.`; case "unrefreshable-denomination": return i18n.str`Some coins, including small change, cannot be renewed with the current offerings.`; case "invalid-lifetime": return i18n.str`The exchange provided invalid coin validity dates.`; case "nonpositive-refresh-interval": - return i18n.str`The current coin lifetimes do not support an annual cost bound.`; + return /* Translators: The bound is a conditional maximum for the cost of renewing digital cash over a year; it is not a promised fee or an expected charge. */ i18n.str`The current coin lifetimes do not support an annual cost bound.`; case "pending-refresh": case "unsettled-balance": return i18n.str`The balance has operations that have not settled yet.`; default: - return i18n.str`There is not enough denomination information to calculate a bound.`; + return /* Translators: A denomination is a digital-coin type defined by its value, validity period, and exchange signing key. */ i18n.str`There is not enough denomination information to calculate a bound.`; } } @@ -97,7 +97,9 @@ function balanceView(balance: WalletBalance): BalanceView { } const warnings: string[] = []; if (balance.scopeInfo.type === ScopeType.ExchangeLegacyKeys) { - warnings.push(i18n.str`Issued under superseded exchange keys`); + warnings.push( + /* Translators: Legacy funds belong to a previous cryptographic identity of an exchange, not merely an old wallet software version. */ i18n.str`Issued under superseded exchange keys`, + ); } if (balance.flags.includes(BalanceFlag.IncomingKyc)) { warnings.push(i18n.str`Incoming funds need identity verification`); diff --git a/packages/wallet-webui/src/routes/deposit-model.ts b/packages/wallet-webui/src/routes/deposit-model.ts @@ -62,16 +62,16 @@ export function depositDestinationView( summary = payto.address; break; case PaytoType.TalerReserve: - method = i18n.str`Taler reserve`; + method = /* Translators: A reserve is an exchange-side account used to withdraw digital cash, not the wallet's spendable balance. */ i18n.str`Taler reserve`; fields.push({ label: i18n.str`Exchange`, value: payto.exchange }); fields.push({ - label: i18n.str`Reserve`, + label: /* Translators: A reserve is an exchange-side account used to withdraw digital cash, not the wallet's spendable balance. */ i18n.str`Reserve`, value: encodeCrock(payto.reservePub), }); summary = encodeCrock(payto.reservePub); break; case PaytoType.TalerReserveHttp: - method = i18n.str`Taler reserve (HTTP)`; + method = /* Translators: A reserve is an exchange-side account used to withdraw digital cash, not the wallet's spendable balance. */ i18n.str`Taler reserve (HTTP)`; fields.push({ label: i18n.str`Exchange`, value: payto.exchange }); fields.push({ label: i18n.str`Reserve`, diff --git a/packages/wallet-webui/src/routes/exchange-gate-model.ts b/packages/wallet-webui/src/routes/exchange-gate-model.ts @@ -15,10 +15,10 @@ function exchangeActionGate( if (hasUnconfirmedKeyChange) { const reason = action === "withdrawal" - ? i18n.str`This exchange changed its signing keys. Verify that change before withdrawing.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This exchange changed its signing keys. Verify that change before withdrawing.` : action === "request" - ? i18n.str`This exchange changed its signing keys. Verify that change before creating this request.` - : i18n.str`This exchange changed its signing keys. Verify that change before receiving this payment.`; + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This exchange changed its signing keys. Verify that change before creating this request.` + : /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This exchange changed its signing keys. Verify that change before receiving this payment.`; return { reason, label: i18n.str`Verify key change`, @@ -32,10 +32,10 @@ function exchangeActionGate( ) { const reason = action === "withdrawal" - ? i18n.str`Review and accept this exchange's terms of service before withdrawing.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Review and accept this exchange's terms of service before withdrawing.` : action === "request" - ? i18n.str`Review and accept this exchange's terms of service before creating this request.` - : i18n.str`Review and accept this exchange's terms of service before receiving this payment.`; + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Review and accept this exchange's terms of service before creating this request.` + : /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Review and accept this exchange's terms of service before receiving this payment.`; return { reason, label: i18n.str`Review terms`, diff --git a/packages/wallet-webui/src/routes/exchange-key-recovery-model.ts b/packages/wallet-webui/src/routes/exchange-key-recovery-model.ts @@ -54,11 +54,11 @@ export function exchangeKeyRecoveryFromInsufficientBalance( return { kind: "verify", exchangeBaseUrls: pending, - message: i18n.str`Some funds are locked until you verify an exchange signing-key change. Review the transfer again afterward because older denominations might remain unusable.`, + message: /* Translators: A denomination is a digital-coin type defined by its value, validity period, and exchange signing key. */ i18n.str`Some funds are locked until you verify an exchange signing-key change. Review the transfer again afterward because older denominations might remain unusable.`, actionLabel: pending.length === 1 ? i18n.str`Verify key change` - : i18n.str`Review exchanges`, + : /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Review exchanges`, }; } return { @@ -91,7 +91,7 @@ export function exchangeKeyRecoveryFromError( return { kind: "verify", exchangeBaseUrls: [exchangeBaseUrl], - message: i18n.str`This operation is blocked until you verify the exchange signing-key change.`, + message: /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This operation is blocked until you verify the exchange signing-key change.`, actionLabel: i18n.str`Verify key change`, }; } diff --git a/packages/wallet-webui/src/routes/import-model.ts b/packages/wallet-webui/src/routes/import-model.ts @@ -19,7 +19,7 @@ export function inspectImportDump( if (isRecord(dump.databases)) { if (activeBackend !== "indexeddb") throw Error( - i18n.str`This IndexedDB export must be converted before importing into the active backend.`, + /* Translators: Backend here means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`This IndexedDB export must be converted before importing into the active backend.`, ); const mainDatabase = Object.entries(dump.databases).find(([name]) => name.startsWith("taler-wallet-main-"), diff --git a/packages/wallet-webui/src/routes/management-model.ts b/packages/wallet-webui/src/routes/management-model.ts @@ -119,13 +119,13 @@ export type BankAccountSaveValues = { export function exchangeEntryStatusLabel(status: string): string { switch (status) { case "preset": - // Exchange entry provided by the wallet's built-in configuration. + // Translators: Exchange entry provided by the wallet's built-in configuration. return i18n.str`Built in`; case "ephemeral": - // Exchange discovered temporarily and not yet used by the wallet. + // Translators: Exchange discovered temporarily and not yet used by the wallet. return i18n.str`Discovered`; case "used": - // Exchange that the wallet has used for digital cash. + // Translators: Exchange that the wallet has used for digital cash. return i18n.str`In use`; default: return status; @@ -139,9 +139,9 @@ export function exchangeUpdateStatusLabel(status: string): string { case "initial-update": return i18n.str`Initial update in progress`; case "suspended": - return i18n.str`Updates suspended`; + return /* Translators: Automatic refreshes of exchange information are paused. */ i18n.str`Updates suspended`; case "unavailable-update": - return i18n.str`Update unavailable`; + return /* Translators: The exchange information could not be fetched; not an unavailable app update. */ i18n.str`Update unavailable`; case "ready": // Translators: Describes an exchange whose information is ready for use. return i18n.ctx("exchange update status")`Ready`; @@ -157,9 +157,9 @@ export function exchangeUpdateStatusLabel(status: string): string { export function exchangeTosStatusLabel(status: string): string { switch (status) { case "pending": - return i18n.str`Terms pending`; + return /* Translators: The wallet is still fetching the exchange's terms of service. */ i18n.str`Terms pending`; case "proposed": - return i18n.str`Terms require review`; + return /* Translators: The user has not accepted the exchange's current terms of service. */ i18n.str`Terms require review`; case "accepted": return i18n.str`Terms accepted`; case "missing-tos": @@ -172,11 +172,13 @@ export function exchangeTosStatusLabel(status: string): string { export function exchangeListView(exchange: ExchangeListItem): ExchangeView { const warnings: string[] = []; if (exchange.unconfirmedKeyChange) - warnings.push(i18n.str`Master key changed; verify it before withdrawing.`); + warnings.push( + /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Master key changed; verify it before withdrawing.`, + ); if (exchange.lastUpdateErrorInfo) warnings.push( exchange.lastUpdateErrorInfo.error.hint ?? - i18n.str`The last exchange update failed.`, + /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The last exchange update failed.`, ); if (exchange.tosStatus === "proposed") warnings.push(i18n.str`Updated terms require review.`); @@ -248,7 +250,9 @@ export function exchangeUrlFromRoute(id: string): string { try { return decodeURIComponent(id); } catch { - throw Error(i18n.str`The exchange route is malformed.`); + throw Error( + /* Translators: Route means a navigation URL within the wallet interface, not a bank-transfer path. */ i18n.str`The exchange route is malformed.`, + ); } } diff --git a/packages/wallet-webui/src/routes/money-flow-model.ts b/packages/wallet-webui/src/routes/money-flow-model.ts @@ -43,7 +43,9 @@ export function validatedPaytoUri(value: string): string { const uri = value.trim(); const parsed = Paytos.parsePaytoUri(uri); if (!parsed?.targetType) - throw Error(i18n.str`Enter a valid supported payto URI.`); + throw Error( + /* Translators: payto:// is the URI scheme for transfer-account details. Preserve protocol and method names such as iban and x-taler-bank. */ i18n.str`Enter a valid supported payto URI.`, + ); return uri; } diff --git a/packages/wallet-webui/src/routes/payment-model.ts b/packages/wallet-webui/src/routes/payment-model.ts @@ -136,13 +136,13 @@ export function paymentReviewView( if (tokens?.tokensUntrusted) { tokenWarning = i18n.plural( tokens.tokensUntrusted, - i18n.lazy`${tokens.tokensUntrusted} requested token is not trusted by this wallet.`, + /* Translators: Token means a merchant-issued discount, access pass, or receipt used in a purchase, not an authentication credential. */ i18n.lazy`${tokens.tokensUntrusted} requested token is not trusted by this wallet.`, i18n.lazy`${tokens.tokensUntrusted} requested tokens are not trusted by this wallet.`, ); } else if (tokens?.tokensUnexpected) { tokenWarning = i18n.plural( tokens.tokensUnexpected, - i18n.lazy`${tokens.tokensUnexpected} token has an unexpected merchant domain. Review before paying.`, + /* Translators: Token means a merchant-issued discount, access pass, or receipt used in a purchase, not an authentication credential. */ i18n.lazy`${tokens.tokensUnexpected} token has an unexpected merchant domain. Review before paying.`, i18n.lazy`${tokens.tokensUnexpected} tokens have an unexpected merchant domain. Review before paying.`, ); } @@ -293,7 +293,7 @@ function paymentUnavailableReason( needsToken: boolean, ): string { if (needsToken && !detail) { - return i18n.str`A required token is not available in this wallet.`; + return /* Translators: Token means a merchant-issued discount, access pass, or receipt used in a purchase, not an authentication credential. */ i18n.str`A required token is not available in this wallet.`; } return insufficientBalanceMessage(detail); } @@ -303,11 +303,11 @@ function tokenAvailabilityWarning( ): string | undefined { switch (hint) { case TokenAvailabilityHint.WalletTokensAvailableInsufficient: - return i18n.str`Not enough of this token is available.`; + return /* Translators: Token means a merchant-issued discount, access pass, or receipt used in a purchase, not an authentication credential. */ i18n.str`Not enough of this token is available.`; case TokenAvailabilityHint.MerchantUnexpected: - return i18n.str`This merchant is not an expected recipient for this token.`; + return /* Translators: Token means a merchant-issued discount, access pass, or receipt used in a purchase, not an authentication credential. */ i18n.str`This merchant is not an expected recipient for this token.`; case TokenAvailabilityHint.MerchantUntrusted: - return i18n.str`This merchant is not trusted to receive this token.`; + return /* Translators: Token means a merchant-issued discount, access pass, or receipt used in a purchase, not an authentication credential. */ i18n.str`This merchant is not trusted to receive this token.`; case undefined: return undefined; } diff --git a/packages/wallet-webui/src/routes/scope-model.ts b/packages/wallet-webui/src/routes/scope-model.ts @@ -66,9 +66,9 @@ export function scopeLabel(scope: ScopeInfo): string { case ScopeType.Exchange: return i18n.str`${scope.currency} via ${scopeProviderLabel(scope.url)}`; case ScopeType.Auditor: - return i18n.str`${scope.currency} audited by ${scopeProviderLabel(scope.url)}`; + return /* Translators: %1$s identifies a currency or exchange; %2$s names its independent auditor. */ i18n.str`${scope.currency} audited by ${scopeProviderLabel(scope.url)}`; case ScopeType.ExchangeLegacyKeys: - return i18n.str`${scope.currency} via ${scopeProviderLabel(scope.url)} (legacy keys)`; + return /* Translators: %1$s identifies the currency; %2$s identifies the exchange or provider. The funds use superseded exchange keys. */ i18n.str`${scope.currency} via ${scopeProviderLabel(scope.url)} (legacy keys)`; } } diff --git a/packages/wallet-webui/src/routes/token-model.ts b/packages/wallet-webui/src/routes/token-model.ts @@ -84,7 +84,7 @@ export function tokenListView( ? i18n.str`Valid until ${formatDate(item.validityEnd, language)}` : state === "future" ? i18n.str`Valid from ${formatDate(item.validityStart, language)}` - : i18n.str`Validity period ${formatDate(item.validityStart, language)} – ${formatDate(item.validityEnd, language)}`; + : /* Translators: %1$s and %2$s are the start and end dates of a merchant-issued token's validity. */ i18n.str`Validity period ${formatDate(item.validityStart, language)} – ${formatDate(item.validityEnd, language)}`; const quantity = kind === "discounts" && "tokensAvailable" in item ? item.tokensAvailable diff --git a/packages/wallet-webui/src/routes/transaction-model.ts b/packages/wallet-webui/src/routes/transaction-model.ts @@ -84,12 +84,12 @@ function typePresentations(): Record< > { return { [TransactionType.Withdrawal]: { - label: i18n.str`Withdrawal`, + label: /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdrawal`, icon: "↓", direction: "credit", }, [TransactionType.InternalWithdrawal]: { - label: i18n.str`Internal withdrawal`, + label: /* Translators: A digital-cash withdrawal initiated internally as part of another wallet operation. */ i18n.str`Internal withdrawal`, icon: "↓", direction: "credit", }, @@ -118,16 +118,19 @@ function typePresentations(): Record< direction: "debit", }, [TransactionType.PeerPushDebit]: { + // Translators: Direction of a peer transfer, including pending transfers. label: i18n.str`Outgoing transfer`, icon: "↗", direction: "debit", }, [TransactionType.PeerPushCredit]: { + // Translators: Direction of a peer transfer, including pending transfers. label: i18n.str`Incoming transfer`, icon: "↙", direction: "credit", }, [TransactionType.PeerPullDebit]: { + // Translators: Payment of another person's request; it may still be pending. label: i18n.str`Payment`, icon: "↗", direction: "debit", @@ -184,12 +187,12 @@ function titleAndSubtitle(transaction: Transaction): { case TransactionType.InternalWithdrawal: return { title: host(transaction.exchangeBaseUrl) ?? i18n.str`Withdrawal`, - subtitle: i18n.str`Digital cash withdrawal`, + subtitle: /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Digital cash withdrawal`, }; case TransactionType.Deposit: { const destination = depositDestinationView(transaction.targetPaytoUri); return { - title: i18n.str`Deposit to bank account`, + title: /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`Deposit to bank account`, subtitle: destination.title === destination.summary ? destination.summary @@ -201,7 +204,11 @@ function titleAndSubtitle(transaction: Transaction): { return { title: transaction.info.summary ?? i18n.str`Payment request` }; case TransactionType.PeerPushCredit: case TransactionType.PeerPushDebit: - return { title: transaction.info.summary ?? i18n.str`Transfer` }; + return { + title: + transaction.info.summary ?? + /* Translators: A transfer of digital cash between people using Taler wallets. */ i18n.str`Transfer`, + }; case TransactionType.Refresh: return { title: i18n.str`Digital cash renewal`, @@ -223,14 +230,14 @@ function minorLabels(): Partial<Record<TransactionMinorState, string>> { return { [TransactionMinorState.KycRequired]: i18n.str`Identity verification required`, [TransactionMinorState.KycInit]: i18n.str`Loading identity verification requirements`, - [TransactionMinorState.BalanceKycRequired]: i18n.str`Balance limit verification required`, - [TransactionMinorState.KycAuthRequired]: i18n.str`Bank account verification required`, + [TransactionMinorState.BalanceKycRequired]: /* Translators: Identity verification required because of an exchange limit on funds held or received. */ i18n.str`Balance limit verification required`, + [TransactionMinorState.KycAuthRequired]: /* Translators: The user must prove control of a bank account before the operation can continue. */ i18n.str`Bank account verification required`, [TransactionMinorState.BankConfirmTransfer]: i18n.str`Waiting for bank confirmation`, - [TransactionMinorState.BankRegisterReserve]: i18n.str`Registering withdrawal with bank`, + [TransactionMinorState.BankRegisterReserve]: /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Registering withdrawal with bank`, [TransactionMinorState.ExchangeWaitReserve]: i18n.str`Waiting for bank transfer`, - [TransactionMinorState.Withdraw]: i18n.str`Withdrawing digital coins`, + [TransactionMinorState.Withdraw]: /* Translators: The exchange is issuing digital cash into the wallet. */ i18n.str`Withdrawing digital coins`, [TransactionMinorState.AcceptRefund]: i18n.str`Processing refund`, - [TransactionMinorState.CheckRefund]: i18n.str`Checking for refund`, + [TransactionMinorState.CheckRefund]: /* Translators: Checking whether the merchant offers a refund for an existing payment. */ i18n.str`Checking for refund`, [TransactionMinorState.AutoRefund]: i18n.str`Processing automatic refund`, [TransactionMinorState.CompletedByOtherWallet]: i18n.str`Completed by another wallet`, [TransactionMinorState.ContinuedWithOtherWallet]: i18n.str`Continued in another wallet`, @@ -239,8 +246,8 @@ function minorLabels(): Partial<Record<TransactionMinorState, string>> { [TransactionMinorState.Ready]: i18n.ctx("transaction minor state")`Ready`, [TransactionMinorState.RebindSession]: i18n.str`Restoring merchant session`, [TransactionMinorState.SubmitPayment]: i18n.str`Submitting payment`, - [TransactionMinorState.Refresh]: i18n.str`Renewing digital cash`, - [TransactionMinorState.Track]: i18n.str`Tracking bank transfer`, + [TransactionMinorState.Refresh]: /* Translators: Reissuing digital coins to preserve their value or obtain change; not reloading the UI. */ i18n.str`Renewing digital cash`, + [TransactionMinorState.Track]: /* Translators: Waiting for and checking the bank transfer from the exchange to the deposit destination. */ i18n.str`Tracking bank transfer`, [TransactionMinorState.WaitingForOtherWallet]: i18n.str`Waiting for another wallet`, }; } @@ -262,7 +269,9 @@ export function transactionStateView(transaction: Transaction): { }; case TransactionMajorState.Pending: return { - label: minor ?? i18n.str`Pending`, + label: + minor ?? + /* Translators: An operation is not complete and is waiting for processing or an external event. */ i18n.str`Pending`, detail: transaction.txState.working ? i18n.str`The wallet is processing this transaction now.` : i18n.str`The wallet is waiting for another service or event.`, @@ -280,19 +289,25 @@ export function transactionStateView(transaction: Transaction): { }; case TransactionMajorState.Finalizing: return { - label: minor ?? i18n.str`Finalizing`, + label: + minor ?? + /* Translators: The main transaction step has finished, but the wallet still has background work to complete. */ i18n.str`Finalizing`, detail: i18n.str`The visible operation is complete while the wallet finishes background work.`, tone: "neutral", }; case TransactionMajorState.Aborting: return { - label: minor ?? i18n.str`Aborting`, + label: + minor ?? + /* Translators: Cancellation is in progress; the transaction has not yet reached its final cancelled state. */ i18n.str`Aborting`, detail: i18n.str`The wallet is safely stopping this transaction.`, tone: "warning", }; case TransactionMajorState.Aborted: return { - label: minor ?? i18n.str`Aborted`, + label: + minor ?? + /* Translators: The transaction was cancelled before successful completion. */ i18n.str`Aborted`, detail: transaction.txState.minor === TransactionMinorState.ContinuedWithOtherWallet @@ -304,7 +319,9 @@ export function transactionStateView(transaction: Transaction): { case TransactionMajorState.SuspendedFinalizing: case TransactionMajorState.SuspendedAborting: return { - label: minor ? i18n.str`Suspended — ${minor}` : i18n.str`Suspended`, + label: minor + ? /* Translators: A paused transaction. %1$s is a translated description of its current processing step. */ i18n.str`Suspended — ${minor}` + : /* Translators: The user or wallet paused network processing; the transaction may be resumed. */ i18n.str`Suspended`, detail: i18n.str`Network processing is paused. Resume when you are ready.`, tone: "warning", }; @@ -322,13 +339,13 @@ export function transactionStateView(transaction: Transaction): { }; case TransactionMajorState.None: return { - label: i18n.str`Starting`, + label: /* Translators: The transaction exists but its processing has not started yet. */ i18n.str`Starting`, detail: i18n.str`The transaction has not started processing yet.`, tone: "neutral", }; case TransactionMajorState.Deleted: return { - label: i18n.str`Deleted`, + label: /* Translators: The transaction history record was deleted; this label does not describe the wallet balance. */ i18n.str`Deleted`, detail: i18n.str`The transaction record was deleted.`, tone: "neutral", }; @@ -376,7 +393,10 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { case TransactionType.Payment: if (transaction.info) { fields.push( - { label: i18n.str`Purchase price`, value: transaction.amountRaw }, + { + label: /* Translators: The nominal order price, before additional wallet payment fees. */ i18n.str`Purchase price`, + value: transaction.amountRaw, + }, { label: i18n.str`Wallet spends`, value: transaction.amountEffective, @@ -396,7 +416,7 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { }); if (transaction.refundPending) fields.push({ - label: i18n.str`Refund available`, + label: /* Translators: A refund offered for collection, not yet credited to this wallet. */ i18n.str`Refund available`, value: transaction.refundPending, }); break; @@ -404,12 +424,12 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { fields.push( { label: i18n.str`Refund amount`, value: transaction.amountRaw }, { - label: i18n.str`Expected wallet credit`, + label: /* Translators: The expected amount added to this wallet after fees, not its total balance or a credit limit. */ i18n.str`Expected wallet credit`, value: transaction.amountEffective, }, ); fields.push({ - label: i18n.str`Original payment`, + label: /* Translators: The earlier wallet transaction to which this refund belongs. */ i18n.str`Original payment`, value: transaction.refundedTransactionId, breakAll: true, }); @@ -433,11 +453,11 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { breakAll: true, }); fields.push({ - label: i18n.str`Method`, + label: /* Translators: The way a digital-cash withdrawal is funded, such as a manual bank transfer. */ i18n.str`Method`, value: transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer ? i18n.str`Manual bank transfer` - : i18n.str`Integrated bank withdrawal`, + : /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Integrated bank withdrawal`, }); break; case TransactionType.Deposit: { @@ -466,13 +486,13 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { }); if (transaction.kycAuthTransferInfo) { fields.push({ - label: i18n.str`Verification source account`, + label: /* Translators: The bank account from which the small ownership-verification transfer must be sent. */ i18n.str`Verification source account`, value: depositDestinationView( transaction.kycAuthTransferInfo.debitPaytoUri, ).summary, }); fields.push({ - label: i18n.str`Verification account key`, + label: /* Translators: The cryptographic identifier used for the bank-account verification transfer. */ i18n.str`Verification account key`, value: transaction.kycAuthTransferInfo.accountPub, breakAll: true, }); @@ -574,7 +594,10 @@ function fieldsFor(transaction: Transaction): TransactionDetailField[] { break; case TransactionType.Recoup: fields.push( - { label: i18n.str`Recovery amount`, value: transaction.amountRaw }, + { + label: /* Translators: Value of revoked digital coins being recovered. The recovery may still be pending. */ i18n.str`Recovery amount`, + value: transaction.amountRaw, + }, { label: i18n.str`Expected wallet credit`, value: transaction.amountEffective, @@ -611,17 +634,17 @@ function actionPresentations(): Record< id: "abort", label: i18n.str`Abort`, dangerous: true, - confirmation: i18n.str`Abort this transaction? The wallet may need to contact the bank, merchant, or exchange to stop it safely.`, + confirmation: /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Abort this transaction? The wallet may need to contact the bank, merchant, or exchange to stop it safely.`, }, [TransactionAction.Fail]: { id: "fail", - label: i18n.str`Stop and mark failed`, + label: /* Translators: Force the transaction into a failed state; this action can lose money and cannot be undone. */ i18n.str`Stop and mark failed`, dangerous: true, confirmation: i18n.str`Stop this transaction and mark it failed? This can result in loss of funds and cannot be undone.`, }, [TransactionAction.Delete]: { id: "delete", - label: i18n.str`Delete record`, + label: /* Translators: Delete a transaction-history entry, not the money resulting from the transaction. */ i18n.str`Delete record`, dangerous: true, confirmation: i18n.str`Permanently delete this transaction record? Resulting funds are not deleted, but the history entry cannot be restored.`, }, @@ -686,7 +709,7 @@ export function transactionDetailView( id: "delete" as const, label: i18n.str`Cancel payment`, dangerous: true, - confirmation: i18n.str`Cancel this payment? The wallet will remove it from transaction history. The merchant may keep the order claimed until it expires.`, + confirmation: /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`Cancel this payment? The wallet will remove it from transaction history. The merchant may keep the order claimed until it expires.`, }; } return presentations[action]; @@ -702,7 +725,7 @@ export function transactionDetailView( ) actions.push({ id: "refund", - label: i18n.str`Collect refund`, + label: /* Translators: Accept a refund already offered by the merchant, not ask the merchant to authorize a new refund. */ i18n.str`Collect refund`, dangerous: false, }); } catch { @@ -713,7 +736,7 @@ export function transactionDetailView( if (base.pending && safeWebUrl(transaction.kycUrl)) externalAction = { heading: i18n.str`Identity verification required`, - description: i18n.str`Complete the required identity verification on the exchange's website.`, + description: /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Complete the required identity verification on the exchange's website.`, label: i18n.str`Complete verification`, url: transaction.kycUrl, }; @@ -726,7 +749,7 @@ export function transactionDetailView( ) { externalAction = { heading: i18n.str`Bank confirmation required`, - description: i18n.str`Confirm this withdrawal on your bank's website.`, + description: /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Confirm this withdrawal on your bank's website.`, label: i18n.str`Continue at bank`, url: transaction.withdrawalDetails.bankConfirmationUrl, }; @@ -760,7 +783,7 @@ export function transactionDetailView( ) { continuationAction = { kind: "deposit-kyc-auth", - label: i18n.str`View authentication transfer`, + label: /* Translators: Bank-account authentication uses a small transfer from the specified account to prove control of it. KYC means identity verification. */ i18n.str`View authentication transfer`, }; } const order = @@ -791,7 +814,7 @@ export function transactionDetailView( transaction.type === TransactionType.InternalWithdrawal ) { technicalFields.push({ - label: i18n.str`Reserve public key`, + label: /* Translators: A reserve is an exchange-side account used to withdraw digital cash, not the wallet's spendable balance. */ i18n.str`Reserve public key`, value: transaction.withdrawalDetails.reservePub, breakAll: true, copyable: true, @@ -799,14 +822,14 @@ export function transactionDetailView( } if (transaction.type === TransactionType.Deposit) { technicalFields.push({ - label: i18n.str`Payto URI`, + label: /* Translators: payto:// is the URI scheme for transfer-account details. Preserve protocol and method names such as iban and x-taler-bank. */ i18n.str`Payto URI`, value: transaction.targetPaytoUri, breakAll: true, copyable: true, }); if (transaction.kycAuthTransferInfo) { technicalFields.push({ - label: i18n.str`Verification source payto URI`, + label: /* Translators: payto:// is the URI scheme for transfer-account details. Preserve protocol and method names such as iban and x-taler-bank. */ i18n.str`Verification source payto URI`, value: transaction.kycAuthTransferInfo.debitPaytoUri, breakAll: true, copyable: true, @@ -819,7 +842,7 @@ export function transactionDetailView( ); if (parsedTransactionId?.tag === TransactionType.PeerPushDebit) { technicalFields.push({ - label: i18n.str`Purse public key`, + label: /* Translators: A purse is an exchange-side container used for a peer payment, not the user's entire wallet. */ i18n.str`Purse public key`, value: parsedTransactionId.pursePub, breakAll: true, copyable: true, @@ -846,15 +869,15 @@ export function transactionDetailView( { label: base.direction === "credit" - ? i18n.str`Final amount received` - : i18n.str`Final cost`, + ? /* Translators: The settled amount credited to the wallet, after fees. */ i18n.str`Final amount received` + : /* Translators: The settled total debited from the wallet, including fees. */ i18n.str`Final cost`, value: transaction.amountEffectiveFinal, }, ] : base.amountUnavailable ? [ { - label: i18n.str`Final amount`, + label: /* Translators: The settled transaction amount; it is unavailable until settlement can be established. */ i18n.str`Final amount`, value: i18n.str`Unavailable until settlement can be established`, }, ] diff --git a/packages/wallet-webui/src/routes/withdrawal-model.ts b/packages/wallet-webui/src/routes/withdrawal-model.ts @@ -209,7 +209,7 @@ export function wireOptions( account.bankLabel && account.transferOptions.length > 1 ? option.type === "ch-qr-bill" ? i18n.str`${account.bankLabel} — Swiss QR bill` - : i18n.str`${account.bankLabel} — bank transfer details` + : /* Translators: %1$s is a label identifying one of the exchange's bank-transfer options. */ i18n.str`${account.bankLabel} — bank transfer details` : (account.bankLabel ?? (option.type === "ch-qr-bill" ? i18n.str`Swiss QR bill` @@ -271,7 +271,7 @@ function accountRestrictionView( if (restrictions.some((restriction) => restriction.type === "deny")) { return { hints, - unavailableReason: i18n.str`This exchange account is not available for incoming transfers.`, + unavailableReason: /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This exchange account is not available for incoming transfers.`, }; } if (!context.sourcePaytoUri) { @@ -288,7 +288,7 @@ function accountRestrictionView( hints, ...(hasInvalidPattern ? { - unavailableReason: i18n.str`This exchange account has an invalid source-account restriction.`, + unavailableReason: /* Translators: The source account is the bank account sending the transfer, not the recipient account at the exchange. */ i18n.str`This exchange account has an invalid source-account restriction.`, } : {}), }; @@ -304,7 +304,7 @@ function accountRestrictionView( : hints, ...(!result.ok ? { - unavailableReason: i18n.str`This exchange account does not accept transfers from the required source account.`, + unavailableReason: /* Translators: The source account is the bank account sending the transfer, not the recipient account at the exchange. */ i18n.str`This exchange account does not accept transfers from the required source account.`, } : {}), }; diff --git a/packages/wallet-webui/src/routes/withdrawal-review-model.ts b/packages/wallet-webui/src/routes/withdrawal-review-model.ts @@ -66,19 +66,19 @@ export function withdrawalReviewModel( const trustError = trustErrorMessage ?? (!trustLoading && !managedExchange - ? i18n.str`The wallet could not verify the selected exchange. Go back and select it again.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The wallet could not verify the selected exchange. Go back and select it again.` : undefined); const volumeStatus = prepared.details.withdrawalKycStatus; const blockedReason = (trustLoading - ? i18n.str`Checking the exchange's keys and terms…` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Checking the exchange's keys and terms…` : trustError) ?? gate?.reason ?? (volumeStatus === "hard-limit" - ? i18n.str`This withdrawal would exceed the exchange’s withdrawal limit. Choose a smaller amount or try again later.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This withdrawal would exceed the exchange’s withdrawal limit. Choose a smaller amount or try again later.` : undefined) ?? (accountCount === 0 - ? i18n.str`The exchange did not provide a usable bank account for this withdrawal.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange did not provide a usable bank account for this withdrawal.` : undefined); const overSoftKyc = prepared.details.kycRequired ?? @@ -91,13 +91,13 @@ export function withdrawalReviewModel( volumeStatus === "hard-limit" ? undefined : volumeStatus === "kyc-required" - ? i18n.str`This withdrawal, together with your recent withdrawals, may require identity verification at the exchange.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This withdrawal, together with your recent withdrawals, may require identity verification at the exchange.` : overSoftKyc - ? i18n.str`This amount may require identity verification at the exchange.` + ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`This amount may require identity verification at the exchange.` : undefined; const limitInfo = !warning && volumeStatus === "unknown" - ? i18n.str`Withdrawal limits could not be checked. Identity verification may be required.` + ? /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdrawal limits could not be checked. Identity verification may be required.` : undefined; const usage = prepared.details.balanceKyc; const balanceKyc = @@ -108,7 +108,7 @@ export function withdrawalReviewModel( : undefined; const coinCountWarning = prepared.details.numCoins > 100 - ? i18n.str`This withdrawal creates more than 100 digital coins and may take longer than usual.` + ? /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`This withdrawal creates more than 100 digital coins and may take longer than usual.` : undefined; return { fee, diff --git a/packages/wallet-webui/src/ui/RequestProgress.tsx b/packages/wallet-webui/src/ui/RequestProgress.tsx @@ -32,7 +32,9 @@ export function RequestProgressPanel(props: { return ( <aside aria-live="polite" - aria-label={i18n.str`Request progress`} + aria-label={ + /* Translators: Progress of a background request to a service, not a request for another person to pay. */ i18n.str`Request progress` + } class={ props.embedded ? "max-w-xl" @@ -44,9 +46,9 @@ export function RequestProgressPanel(props: { {progress.phase === "stalled" ? i18n.str`The service is not responding` : progress.phase === "cancelling" - ? i18n.str`Cancelling request…` + ? /* Translators: Stopping an in-flight service request; this alone does not cancel the financial transaction. */ i18n.str`Cancelling request…` : progress.phase === "retrying" - ? i18n.str`Retrying request…` + ? /* Translators: Repeating a failed background service request. */ i18n.str`Retrying request…` : i18n.str`This request is taking longer than expected`} </h2> {progress.phase === "stalled" && progress.error && ( @@ -62,7 +64,9 @@ export function RequestProgressPanel(props: { )} {progress.phase === "stalled" && delay && ( <p class="mt-3 text-sm text-secondary"> - {i18n.str`The wallet will retry automatically in ${delay}.`} + { + /* Translators: %1$s is a localized time duration until the next service-request retry. */ i18n.str`The wallet will retry automatically in ${delay}.` + } </p> )} <div class="mt-4 flex justify-end gap-2"> diff --git a/packages/wallet-webui/src/ui/Shell.tsx b/packages/wallet-webui/src/ui/Shell.tsx @@ -34,8 +34,7 @@ export function Shell(props: { // Translators: Navigation command to deposit wallet funds into a bank // account (cash out), not to add funds to the wallet. const depositLabel = i18n.ctx("wallet action")`Deposit`; - // Translators: Mobile navigation item for all balances and currencies held - // by the wallet, not generic files or property. + // Translators: Mobile navigation item for the wallet's currency balances. const balancesLabel = i18n.str`Balances`; return ( <div class="min-h-screen bg-background pb-24 text-onBackground md:pb-0"> @@ -52,10 +51,9 @@ export function Shell(props: { </Link> {props.demo && <DemoBanner {...props.demo} />} <div class="hidden items-center gap-3 md:flex"> - <span - class="sr-only" - role="status" - >{i18n.str`Wallet host: ${props.connectionLabel}`}</span> + <span class="sr-only" role="status">{ + /* Translators: %1$s describes the local background wallet connection or host, not a remote financial service. */ i18n.str`Wallet host: ${props.connectionLabel}` + }</span> <Link href="/settings" class="rounded-full bg-secondaryContainer px-5 py-3 font-medium text-onSecondaryContainer hover:brightness-95" diff --git a/packages/wallet-webui/src/ui/error.ts b/packages/wallet-webui/src/ui/error.ts @@ -22,7 +22,7 @@ export function localError(message: string): ErrorPresentation { function errorMessage(detail: TalerErrorDetail, fallback: string): string { switch (detail.code) { case TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED: - return i18n.str`This order was already claimed by another wallet.`; + return /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`This order was already claimed by another wallet.`; case TalerErrorCode.WALLET_ORDER_ALREADY_PAID: return i18n.str`This order was already paid.`; case TalerErrorCode.WALLET_MERCHANT_ORDER_NOT_FOUND: @@ -41,7 +41,7 @@ function errorMessage(detail: TalerErrorDetail, fallback: string): string { case TalerErrorCode.GENERIC_TIMEOUT: return i18n.str`The service took too long to respond.`; case TalerErrorCode.WALLET_CORE_NOT_AVAILABLE: - return i18n.str`Wallet-core is currently unavailable.`; + return /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Wallet-core is currently unavailable.`; case TalerErrorCode.WALLET_DB_UNAVAILABLE: return i18n.str`The wallet database is currently unavailable.`; case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: @@ -57,22 +57,22 @@ function errorMessage(detail: TalerErrorDetail, fallback: string): string { case TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED: return i18n.str`This action is not available in the transaction's current state.`; case TalerErrorCode.WALLET_EXCHANGE_TOS_NOT_ACCEPTED: - return i18n.str`Review and accept the exchange terms before continuing.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Review and accept the exchange terms before continuing.`; case TalerErrorCode.WALLET_EXCHANGE_KEYS_NOT_ACCEPTED: - return i18n.str`Review the exchange signing-key change before continuing.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Review the exchange signing-key change before continuing.`; case TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED: - return i18n.str`The exchange requires identity verification before this withdrawal can continue.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange requires identity verification before this withdrawal can continue.`; case TalerErrorCode.WALLET_KYC_LIMIT_EXCEEDED: return i18n.str`This amount exceeds the limit available without identity verification.`; case TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LEGALLY_REFUSED: return i18n.str`The merchant cannot process this payment right now due to legal requirements.`; case TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION: - return i18n.str`The merchant cannot process this payment right now because an exchange transaction limit was reached.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The merchant cannot process this payment right now because an exchange transaction limit was reached.`; case TalerErrorCode.WALLET_NO_SUITABLE_EXCHANGE: - return i18n.str`No compatible exchange is available for this operation.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`No compatible exchange is available for this operation.`; case TalerErrorCode.WALLET_PEER_PULL_DEBIT_PURSE_GONE: case TalerErrorCode.WALLET_PEER_PUSH_CREDIT_PURSE_GONE: - return i18n.str`This peer payment is no longer available.`; + return /* Translators: Peer payments transfer digital cash between people using links. A shared link can offer money to receive or request money to be paid. */ i18n.str`This peer payment is no longer available.`; default: return fallback; } @@ -87,7 +87,7 @@ function errorGuidance(detail: TalerErrorDetail): string | undefined { case TalerErrorCode.WALLET_MERCHANT_ORDER_NOT_FOUND: return i18n.str`Return to the merchant and create a new order.`; case TalerErrorCode.WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE: - return i18n.str`Withdraw funds or choose another payment option, then try again.`; + return /* Translators: Withdrawing adds digital cash to this wallet, usually funded by a bank transfer; it does not cash out the wallet to a bank account. */ i18n.str`Withdraw funds or choose another payment option, then try again.`; case TalerErrorCode.WALLET_NETWORK_ERROR: case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: case TalerErrorCode.GENERIC_TIMEOUT: @@ -145,7 +145,7 @@ export function insufficientBalanceMessage( CoinSelectionFailureReasonType.SupersededExchangeMasterPub, ) ) - return i18n.str`Some funds were issued under legacy exchange keys and cannot be used for this operation.`; + return /* Translators: Legacy funds belong to a previous cryptographic identity of an exchange, not merely an old wallet software version. */ i18n.str`Some funds were issued under legacy exchange keys and cannot be used for this operation.`; const exchangeHints = Array.from( new Set( Object.values(detail.perExchange ?? {}).flatMap((exchange) => @@ -157,13 +157,13 @@ export function insufficientBalanceMessage( detail.causeHint ?? (exchangeHints.length === 1 ? exchangeHints[0] : undefined); if (!causeHint && exchangeHints.length > 1) { - return i18n.str`The wallet's funds have multiple exchange, merchant, or fee restrictions that prevent this payment.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The wallet's funds have multiple exchange, merchant, or fee restrictions that prevent this payment.`; } switch (causeHint) { case InsufficientBalanceHint.MerchantAcceptInsufficient: return i18n.str`The merchant does not accept the funds available in this wallet.`; case InsufficientBalanceHint.MerchantDepositInsufficient: - return i18n.str`The available funds cannot be deposited by this merchant.`; + return /* Translators: A deposit sends digital cash from this wallet to an external account, usually a bank account; it does not add funds to the wallet. */ i18n.str`The available funds cannot be deposited by this merchant.`; case InsufficientBalanceHint.AgeRestricted: return i18n.str`The wallet does not have enough age-restricted funds for this purchase.`; case InsufficientBalanceHint.WalletBalanceMaterialInsufficient: @@ -171,7 +171,7 @@ export function insufficientBalanceMessage( case InsufficientBalanceHint.FeesNotCovered: return i18n.str`The available balance cannot cover the payment and its fees.`; case InsufficientBalanceHint.ExchangeMissingGlobalFees: - return i18n.str`The exchange is missing fee information required for this payment.`; + return /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`The exchange is missing fee information required for this payment.`; case InsufficientBalanceHint.WalletBalanceAvailableInsufficient: case undefined: return detail.balanceAvailable