commit bd9c26c9c18640707aa99edd934e07bbd9979f42
parent 333d04ace7a3714d731c4a84a4df004a02c18779
Author: Florian Dold <dold@taler.net>
Date: Tue, 3 Nov 2026 09:34:31 +0100
wallet-webui: simplify instructions and storage terminology
Diffstat:
10 files changed, 47 insertions(+), 50 deletions(-)
diff --git a/packages/wallet-webui/src/screens/ActionScreen.tsx b/packages/wallet-webui/src/screens/ActionScreen.tsx
@@ -138,7 +138,7 @@ export function ActionScreen(props: {
<p class="mt-3 text-muted">
{props.detail ??
// Translators: Generic action-inbox guidance; the next step varies by Taler URI type.
- i18n.str`Review the wallet-provided details before confirming the next step.`}
+ i18n.str`Check the details before continuing.`}
</p>
<div class="mt-5 flex gap-2">
<Button
diff --git a/packages/wallet-webui/src/screens/BalanceScreen.tsx b/packages/wallet-webui/src/screens/BalanceScreen.tsx
@@ -148,7 +148,7 @@ export function RefreshBalanceDetails(props: {
/* Translators: %1$s is a monetary amount; %2$s identifies its exchange; %3$s is the new validity end date. */ i18n.str`${r.amount} at ${r.exchange} was close to expiration and has been renewed until ${r.expiration}.`
}</p>
<p>{
- /* Translators: %1$s is a date by which the user should reopen the wallet to let it renew expiring digital cash. */ i18n.str`Open the wallet again before ${r.nextDate} to check renewal. This date depends on the coins' validity periods.`
+ /* Translators: %1$s is a date by which the user should reopen the wallet to let it renew expiring digital cash. */ i18n.str`Open the wallet again before ${r.nextDate} to check renewal.`
}</p>
{props.onDismiss && (
<Button
@@ -442,7 +442,7 @@ function BalanceTrend(props: {
))}
</svg>
<figcaption class="sr-only">
- {i18n.str`Hover over or focus the chart to show the start balance, each bucket's balance and completed transaction count, or the current available balance. The graph is reconstructed from visible completed wallet transactions and the current available balance.`}
+ {i18n.str`Hover over or focus the chart to see balances and completed transaction counts. Past balances are estimated from visible completed transactions and your current available balance.`}
</figcaption>
</figure>
);
@@ -764,7 +764,7 @@ export function BalanceScreen(props: {
</Card>
<Card class="flex min-h-52 flex-col justify-between md:px-8 md:py-7">
<div>
- <p>{i18n.str`Get digital cash to experience how to pay with money of the future.`}</p>
+ <p>{i18n.str`Get digital cash to experience how to pay`}</p>
</div>
<Button
disabled={props.gettingDemoCash}
diff --git a/packages/wallet-webui/src/screens/PaymentScreen.tsx b/packages/wallet-webui/src/screens/PaymentScreen.tsx
@@ -643,7 +643,7 @@ export function PaymentScreen(props: {
/* Translators: The order was handed to another wallet; this does not mean that the other wallet has finished paying. */ i18n.str`Payment continued in another wallet`
}</h2>
<p role="status" class="mt-2">{
- /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`The other wallet claimed this payment. You can safely close this screen.`
+ /* Translators: Claiming a merchant order assigns it to a wallet for payment; it does not mean that payment has completed. */ i18n.str`The other wallet claimed this payment. You can close this screen.`
}</p>
<div class="mt-5">
<Button onClick={props.onBack}>{i18n.str`Back to wallet`}</Button>
diff --git a/packages/wallet-webui/src/screens/PeerCreateScreen.tsx b/packages/wallet-webui/src/screens/PeerCreateScreen.tsx
@@ -285,8 +285,8 @@ export function PeerCreateScreen(props: {
<Card>
<p role="alert">
{send
- ? /* Translators: An exchange is a GNU Taler service issuing and redeeming digital cash, not an action that converts one currency into another. */ i18n.str`Your wallet isn’t ready to send money yet. Add an exchange or withdraw digital cash to get 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`Your wallet isn’t ready to request money yet. Add an exchange or withdraw digital cash to get 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`Add an exchange or withdraw digital cash before sending money.`
+ : /* 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 an exchange or withdraw digital cash before requesting money.`}
</p>
</Card>
)}
diff --git a/packages/wallet-webui/src/screens/RecoveryScreen.tsx b/packages/wallet-webui/src/screens/RecoveryScreen.tsx
@@ -32,17 +32,17 @@ export function RecoveryScreen(props: {
<h1 class="mb-4 text-2xl font-bold">{i18n.str`Storage recovery required`}</h1>
<Card>
<p role="alert">{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Wallet storage cannot be opened safely. A stale or unverified generation was not opened automatically.`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`The wallet could not open its stored data.`
}</p>
<p class="mt-3 text-sm text-muted">{props.reason}</p>
{props.backend && (
<dl class="mt-4 grid grid-cols-2 gap-2 text-sm">
<dt class="text-secondary">{
- /* Translators: Backend here means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`Last selected backend`
+ /* Translators: Storage type means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`Last selected storage type`
}</dt>
<dd>{props.backend}</dd>
<dt class="text-secondary">{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Generation`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Stored copy`
}</dt>
<dd>{props.generation}</dd>
</dl>
@@ -52,7 +52,7 @@ export function RecoveryScreen(props: {
<div class="mt-5 border-l-4 border-warning pl-4">
<h2 class="font-semibold">{i18n.str`Storage discovery is incomplete`}</h2>
<p class="mt-2 text-sm text-secondary">{
- /* Translators: Backend here means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`Starting an empty wallet is disabled until every storage backend can be checked safely.`
+ /* Translators: Storage type means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`Starting an empty wallet is disabled until every storage type can be checked safely.`
}</p>
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
{props.recoveryDiscoveryErrors.map((message) => (
@@ -64,15 +64,15 @@ export function RecoveryScreen(props: {
{props.recoverySource && (
<div class="mt-5">
<h2 class="font-semibold">{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Restore the retained wallet generation`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Restore the retained stored wallet copy`
}</h2>
<p class="mt-2 text-sm text-secondary">{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`The previous storage generation is retained and will be verified before it becomes active again.`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`The previous stored copy is retained and will be verified before it becomes active again.`
}</p>
<div class="mt-3">
<Button disabled={props.busy} onClick={props.onRestoreSource}>
{/* Translators: %1$s is the storage backend name; %2$s is a generation number. */}
- {i18n.str`Restore ${props.recoverySource.backend} generation ${props.recoverySource.generation}`}
+ {i18n.str`Restore ${props.recoverySource.backend} stored copy ${props.recoverySource.generation}`}
</Button>
</div>
</div>
@@ -80,10 +80,10 @@ export function RecoveryScreen(props: {
{props.recoveryCandidates && props.recoveryCandidates.length > 0 && (
<div class="mt-5">
<h2 class="font-semibold">{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Discovered wallet generations`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Discovered stored wallet copies`
}</h2>
- {/* Translators: wallet-core is the name of the wallet's background subsystem. */}
- <p class="mt-2 text-sm text-secondary">{i18n.str`Choose only the generation you expect to contain the latest wallet activity. It is verified before wallet-core opens it.`}</p>
+ {/* Translators: A stored copy contains the local wallet database. */}
+ <p class="mt-2 text-sm text-secondary">{i18n.str`Choose the stored copy you expect to contain the latest wallet activity. The wallet checks it before use.`}</p>
<div class="mt-3 space-y-2">
{props.recoveryCandidates.map((candidate) => (
<Button
@@ -93,8 +93,8 @@ export function RecoveryScreen(props: {
onClick={() => props.onRecoverGeneration?.(candidate)}
>
{candidate.backend === "sqlite"
- ? /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Verify and use SQLite generation ${candidate.generation}`
- : /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Verify and use IndexedDB generation ${candidate.generation}`}
+ ? /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Verify and use SQLite stored copy ${candidate.generation}`
+ : /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Verify and use IndexedDB stored copy ${candidate.generation}`}
</Button>
))}
</div>
@@ -104,7 +104,7 @@ export function RecoveryScreen(props: {
(props.recoveryDiscoveryErrors?.length ?? 0) === 0 && (
<div class="mt-5">
<h2 class="font-semibold">{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`No recoverable generation was found`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`No recoverable stored copy was found`
}</h2>
<p class="mt-2 text-sm text-secondary">{i18n.str`Starting a new wallet does not delete inaccessible browser storage. Continue only if this browser session should use an empty wallet.`}</p>
<div class="mt-3">
diff --git a/packages/wallet-webui/src/screens/SettingsScreen.tsx b/packages/wallet-webui/src/screens/SettingsScreen.tsx
@@ -257,9 +257,6 @@ export function SettingsScreen(props: {
<Card>
{/* Translators: Wallet interface language, not the language of an exchange's terms document. */}
<h2 class="font-semibold">{i18n.str`Language`}</h2>
- <p class="my-3 text-sm text-secondary">
- {i18n.str`Choose the language used by this wallet.`}
- </p>
<label class="block">
<span class="mb-1 block text-sm font-medium">
{/* Translators: Language used for the wallet's own controls and messages. */}
@@ -321,8 +318,8 @@ export function SettingsScreen(props: {
/>
<SettingsToggle
label={i18n.str`Allow wallet presence detection`}
- // Translators: callback and talerCallback({ present: true }) are literal web API identifiers/code.
- description={i18n.str`Let pages requesting callback receive talerCallback({ present: true }).`}
+ // Translators: Websites can detect whether a Taler wallet is installed.
+ description={i18n.str`Allow websites to detect that you use a Taler wallet.`}
checked={props.browserIntegration.allowCallback}
disabled={props.browserIntegrationBusy === true}
onChange={(value) =>
@@ -357,14 +354,14 @@ export function SettingsScreen(props: {
) : (
<dl class="mt-3 grid grid-cols-2 gap-2 text-sm">
<dt class="text-secondary">{
- /* Translators: Backend here means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`Backend`
+ /* Translators: Storage type means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`Storage type`
}</dt>
<dd>
{props.backend
? storageBackendLabel(props.backend)
: i18n.str`Loading…`}
</dd>
- <dt class="text-secondary">{i18n.str`Generation`}</dt>
+ <dt class="text-secondary">{i18n.str`Stored copy`}</dt>
<dd>{props.generation ?? i18n.str`Loading…`}</dd>
<dt class="text-secondary">{i18n.str`Migration state`}</dt>
<dd>
@@ -390,8 +387,8 @@ export function SettingsScreen(props: {
}</h2>
<p class="my-3 text-sm text-secondary">
{props.persistence?.durability === "persistent"
- ? /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`The browser has protected this origin from automatic storage eviction. You can still erase it explicitly by clearing site data.`
- : /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`This wallet currently uses best-effort browser storage, which may be removed automatically under storage pressure.`}
+ ? /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Your browser protects wallet data from automatic deletion. Clearing site data will still delete it.`
+ : /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Your browser may delete wallet data automatically when storage space runs low.`}
</p>
{props.persistence?.durability !== "persistent" && (
<div class="flex flex-wrap gap-2">
@@ -501,7 +498,7 @@ export function SettingsScreen(props: {
<Card>
<h2 class="font-semibold">{i18n.str`Database export and import`}</h2>
<p class="my-3 text-sm text-secondary">{
- /* Translators: wallet-core is the background wallet engine; keep the component name unchanged. */ i18n.str`Exports contain sensitive wallet data. Store them securely. Import replaces wallet records only after wallet-core validates the dump.`
+ /* Translators: Imported files contain wallet database records. */ i18n.str`Exports contain sensitive wallet data. Store them securely. Import replaces wallet records only after the wallet validates the file.`
}</p>
<div class="flex flex-wrap gap-2">
<Button
@@ -548,7 +545,7 @@ export function SettingsScreen(props: {
<h2 class="font-semibold">{i18n.str`Stale wallet storage copies`}</h2>
<p class="my-3 text-sm text-secondary">
{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`These inactive generations are not recovery copies tracked by the current wallet. They are never opened or deleted automatically.`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`These inactive stored copies are not recovery copies tracked by the current wallet. They are never opened or deleted automatically.`
}
</p>
<ul class="mb-3 list-inside list-disc text-sm">
@@ -700,7 +697,7 @@ export function SettingsScreen(props: {
<ConfirmationDialog
title={i18n.str`Delete stale wallet storage copies?`}
description={
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Only inactive generations that are not tracked as the active wallet or its current recovery copy will be deleted. This cannot be undone.`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Only inactive stored copies that are not tracked as the active wallet or its current recovery copy will be deleted. This cannot be undone.`
}
cancelLabel={i18n.str`Keep copies`}
confirmLabel={i18n.str`Delete stale copies`}
diff --git a/packages/wallet-webui/src/screens/StorageMigrationCard.tsx b/packages/wallet-webui/src/screens/StorageMigrationCard.tsx
@@ -13,10 +13,10 @@ const stageLabels: Record<StorageMigrationViewStage, string> = {
"requesting-persistence": /* Translators: Browser storage protection prevents automatic deletion of wallet data; it is not encryption or a backup. */ i18n.str`Requesting durable browser storage`,
"stopping-wallet": i18n.str`Stopping wallet activity`,
converting: i18n.str`Copying wallet records`,
- verifying: /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Verifying the new generation`,
- activating: /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Activating the verified generation`,
+ verifying: /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Verifying the new stored copy`,
+ activating: /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Activating the verified stored copy`,
// Translators: wallet-core is the name of the wallet's background subsystem.
- "reopening-wallet": i18n.str`Reopening wallet-core`,
+ "reopening-wallet": i18n.str`Reopening the wallet`,
};
export function storageMigrationStageLabel(
@@ -72,7 +72,7 @@ export function StorageMigrationCard(props: {
<div class="mt-3 space-y-3 text-sm">
<p class="text-secondary">
{
- /* Translators: Backend here means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`SQLite uses the browser's private origin storage and can improve large-wallet operations. Changing this setting converts and verifies the complete wallet before activating the new backend. It never dual-writes or silently opens the retained recovery copy.`
+ /* Translators: Storage type means the local browser storage engine, such as SQLite or IndexedDB, not a remote server. */ i18n.str`SQLite may improve performance for large wallets. Switching copies and checks your wallet data before using the new storage.`
}{" "}
</p>
<div class="flex min-h-12 items-center justify-between gap-4 rounded-xl border border-outlineVariant px-4 py-3">
@@ -89,8 +89,8 @@ export function StorageMigrationCard(props: {
class="block text-xs text-secondary"
>
{props.backend === "sqlite"
- ? i18n.str`On — SQLite is authoritative`
- : i18n.str`Off — IndexedDB is authoritative`}
+ ? i18n.str`On — SQLite is in use`
+ : i18n.str`Off — IndexedDB is in use`}
</span>
</span>
<input
@@ -123,7 +123,7 @@ export function StorageMigrationCard(props: {
{props.backend === "sqlite" && (
<p class="text-xs text-secondary">
{
- /* Translators: A storage generation is a complete version of the local wallet database, not a separate account. */ i18n.str`Turning this off converts the current SQLite wallet into a fresh IndexedDB generation before switching.`
+ /* Translators: A stored copy is a complete version of the local wallet database, not a separate account. */ i18n.str`Turning this off converts the current SQLite wallet into a fresh IndexedDB stored copy before switching.`
}{" "}
</p>
)}
diff --git a/packages/wallet-webui/src/screens/StorybookScreen.tsx b/packages/wallet-webui/src/screens/StorybookScreen.tsx
@@ -295,7 +295,7 @@ export function StorybookScreen(props: {
<Button>{i18n.str`Withdraw digital cash`}</Button>
</Card>
<Card class="flex min-h-52 flex-col justify-between">
- <p>{i18n.str`Get digital cash to experience how to pay with money of the future.`}</p>
+ <p>{i18n.str`Get digital cash to experience how to pay`}</p>
<Button>{i18n.str`Get demo cash`}</Button>
</Card>
</div>
diff --git a/packages/wallet-webui/test/exchange-removal.test.tsx b/packages/wallet-webui/test/exchange-removal.test.tsx
@@ -177,7 +177,7 @@ test("ordinary exchange removal clears currency choices without notifications",
dom.location.hash = `#/peer/${mode}`;
});
await waitFor(() =>
- assert(view.getByText(new RegExp(`isn’t ready to ${mode} money yet`))),
+ assert(view.getByText(new RegExp(`before ${mode === "send" ? "sending" : "requesting"} money`))),
);
assert.equal(
view.queryByRole("combobox", { name: "Currency and payment scope" }),
diff --git a/packages/wallet-webui/test/screens.test.tsx b/packages/wallet-webui/test/screens.test.tsx
@@ -2491,7 +2491,7 @@ test("peer creation explains how to make send and request available", async () =
);
assert.equal(
view.getByRole("alert").textContent,
- "Your wallet isn’t ready to send money yet. Add an exchange or withdraw digital cash to get started.",
+ "Add an exchange or withdraw digital cash before sending money.",
);
view.rerender(
<main>
@@ -2506,7 +2506,7 @@ test("peer creation explains how to make send and request available", async () =
);
assert.equal(
view.getByRole("alert").textContent,
- "Your wallet isn’t ready to request money yet. Add an exchange or withdraw digital cash to get started.",
+ "Add an exchange or withdraw digital cash before requesting money.",
);
cleanup();
await window.happyDOM.abort();
@@ -2544,7 +2544,7 @@ for (const mode of ["send", "request"] as const) {
view.rerender(screen([]));
assert.match(
view.getByRole("alert").textContent ?? "",
- new RegExp(`isn’t ready to ${mode} money yet`),
+ new RegExp(`before ${mode === "send" ? "sending" : "requesting"} money`),
);
assert.equal(view.queryByRole("combobox"), null);
assert.equal(view.queryByRole("button", { name: "Review" }), null);
@@ -4413,14 +4413,14 @@ test("post-activation storage recovery restores the retained source", async () =
);
assert.match(
view.getByRole("alert").textContent ?? "",
- /stale or unverified generation was not opened/,
+ /The wallet could not open its stored data/,
);
- assert(view.getByText("Recovery stage: Verifying the new generation"));
+ assert(view.getByText("Recovery stage: Verifying the new stored copy"));
await userEvent
.setup({ document: window.document as unknown as Document })
.click(
view.getByRole("button", {
- name: "Restore indexeddb generation 1",
+ name: "Restore indexeddb stored copy 1",
}),
);
assert.equal(restored, true);
@@ -4454,12 +4454,12 @@ test("lost storage control requires an explicit discovered generation", async ()
/>
</main>,
);
- assert.ok(view.getByText("Discovered wallet generations"));
+ assert.ok(view.getByText("Discovered stored wallet copies"));
await userEvent
.setup({ document: window.document as unknown as Document })
.click(
view.getByRole("button", {
- name: "Verify and use SQLite generation 4",
+ name: "Verify and use SQLite stored copy 4",
}),
);
assert.deepEqual(selected, { backend: "sqlite", generation: 4 });
@@ -4488,7 +4488,7 @@ test("fully discovered empty storage requires explicit IndexedDB initialization"
/>
</main>,
);
- assert.ok(view.getByText("No recoverable generation was found"));
+ assert.ok(view.getByText("No recoverable stored copy was found"));
await userEvent
.setup({ document: window.document as unknown as Document })
.click(