diff --git a/src/components/ProSupportScreen.tsx b/src/components/ProSupportScreen.tsx
index ca0bb7f..435a452 100644
--- a/src/components/ProSupportScreen.tsx
+++ b/src/components/ProSupportScreen.tsx
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { useTranslation } from '@/contexts/language';
import { useSasquatchPro } from '@/hooks/useSasquatchPro';
import { PRO_TIERS } from '@/lib/sasquatchPro';
-import { DURATION_TIERS, PLATFORM_FEES, FEE_META, tierPriceSats, tierPerDaySats, type FeeType } from '@/hooks/usePlatformFee';
+import { DURATION_TIERS, PLATFORM_FEES, FEE_META, DURATION_TIER_KEYS, FEE_LABEL_KEYS, FEE_BENEFIT_KEYS, tierPriceSats, tierPerDaySats, type FeeType } from '@/hooks/usePlatformFee';
import { SasquatchProDialog } from '@/components/SasquatchProDialog';
import { useLabProfiles } from '@/hooks/useLabProfiles';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@@ -106,13 +106,13 @@ export function ProSupportScreen({ onBack }: { onBack: () => void }) {
{(Object.keys(PLATFORM_FEES) as FeeType[]).map((type) => (
- {FEE_META[type].emoji}{FEE_META[type].label}
- {FEE_META[type].benefit}
+ {FEE_META[type].emoji}{t(FEE_LABEL_KEYS[type])}
+ {t(FEE_BENEFIT_KEYS[type])}
{DURATION_TIERS.map((tier) => (
-
{tier.label}
+
{t(DURATION_TIER_KEYS[tier.id])}
{tierPriceSats(type, tier).toLocaleString()}
{tierPerDaySats(type, tier).toLocaleString()}/{t('settingsUi.proSupport.day')}
diff --git a/src/hooks/useDeleteEntity.ts b/src/hooks/useDeleteEntity.ts
index 4e049cb..54e9078 100644
--- a/src/hooks/useDeleteEntity.ts
+++ b/src/hooks/useDeleteEntity.ts
@@ -4,6 +4,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { isAdminPubkey } from '@/lib/admins';
import { useAdminHide } from '@/hooks/useAdminHide';
import { toast } from '@/hooks/useToast';
+import { useTranslation } from '@/contexts/language';
import type { NostrEvent } from '@nostrify/nostrify';
/**
@@ -43,6 +44,7 @@ function withTimeout
(promise: Promise, ms: number, label: string): Promise
}
export function useDeleteEntity() {
+ const { t } = useTranslation();
const { nostr } = useNostr();
const { user } = useCurrentUser();
const queryClient = useQueryClient();
@@ -105,8 +107,8 @@ export function useDeleteEntity() {
// screen directly — no more silent ContentProvider stall. The signer has its
// own graceful timeout, but we keep an outer guard as a belt-and-braces.
toast({
- title: 'Approve in Amber',
- description: 'Amber will open — tap Approve to confirm the deletion.',
+ title: t('toastsUi.deleteApproveAmber'),
+ description: t('toastsUi.deleteApproveAmberDesc'),
});
if (!user.signer) {
@@ -124,11 +126,11 @@ export function useDeleteEntity() {
const msg = e instanceof Error ? e.message : String(e);
// Distinguish a deliberate rejection from a real failure.
if (/reject/i.test(msg)) {
- toast({ title: 'Cancelled', description: 'You rejected the request in Amber.' });
+ toast({ title: t('toastsUi.deleteCancelled'), description: t('toastsUi.deleteCancelledDesc') });
throw new Error('Signing cancelled.');
}
toast({
- title: "Couldn't sign",
+ title: t('toastsUi.deleteCouldntSign'),
description: msg,
variant: 'destructive',
});
@@ -137,8 +139,8 @@ export function useDeleteEntity() {
// ── PHASE 2: PUBLISH ─────────────────────────────────────────────────────
toast({
- title: 'Step 2/2: Publishing…',
- description: 'Sending the deletion to relays.',
+ title: t('toastsUi.deletePublishing'),
+ description: t('toastsUi.deletePublishingDesc'),
});
try {
@@ -151,8 +153,8 @@ export function useDeleteEntity() {
const msg = e instanceof Error ? e.message : String(e);
// The event was SIGNED fine — the relay is the slow/failing part.
toast({
- title: 'Stuck at PUBLISHING',
- description: `Relay step failed: ${msg}. It was signed OK; the relay didn't confirm.`,
+ title: t('toastsUi.deleteStuck'),
+ description: t('toastsUi.deleteStuckDesc', { error: msg }),
variant: 'destructive',
});
throw new Error(`PUBLISH step failed: ${msg}`);
diff --git a/src/hooks/useNWC.ts b/src/hooks/useNWC.ts
index 8690f45..4831745 100644
--- a/src/hooks/useNWC.ts
+++ b/src/hooks/useNWC.ts
@@ -1,6 +1,7 @@
import { useState, useCallback } from 'react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useToast } from '@/hooks/useToast';
+import { useTranslation } from '@/contexts/language';
import type { LN } from '@getalby/sdk';
/** Marks a payment whose final status could NOT be confirmed after a timeout. */
@@ -50,6 +51,7 @@ export interface NWCInfo {
}
export function useNWCInternal() {
+ const { t } = useTranslation();
const { toast } = useToast();
const [connections, setConnections] = useLocalStorage('nwc-connections', []);
const [activeConnection, setActiveConnection] = useLocalStorage('nwc-active-connection', null);
@@ -73,8 +75,8 @@ export function useNWCInternal() {
const parsed = parseNWCUri(uri);
if (!parsed) {
toast({
- title: 'Invalid NWC URI',
- description: 'Please check the connection string and try again.',
+ title: t('toastsUi.nwcInvalidUri'),
+ description: t('toastsUi.nwcInvalidUriDesc'),
variant: 'destructive',
});
return false;
@@ -83,8 +85,8 @@ export function useNWCInternal() {
const existingConnection = connections.find(c => c.connectionString === parsed.connectionString);
if (existingConnection) {
toast({
- title: 'Connection already exists',
- description: 'This wallet is already connected.',
+ title: t('toastsUi.nwcExists'),
+ description: t('toastsUi.nwcExistsDesc'),
variant: 'destructive',
});
return false;
@@ -135,18 +137,18 @@ export function useNWCInternal() {
setActiveConnection(parsed.connectionString);
toast({
- title: 'Wallet connected',
- description: `Successfully connected to ${connection.alias}.`,
+ title: t('toastsUi.nwcConnected'),
+ description: t('toastsUi.nwcConnectedDesc', { alias: connection.alias ?? '' }),
});
return true;
} catch (error) {
console.error('NWC connection failed:', error);
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ const errorMessage = error instanceof Error ? error.message : t('toastsUi.nwcUnknownError');
toast({
- title: 'Connection failed',
- description: `Could not connect to the wallet: ${errorMessage}`,
+ title: t('toastsUi.nwcFailed'),
+ description: t('toastsUi.nwcFailedDesc', { error: errorMessage }),
variant: 'destructive',
});
return false;
@@ -170,8 +172,8 @@ export function useNWCInternal() {
});
toast({
- title: 'Wallet disconnected',
- description: 'The wallet connection has been removed.',
+ title: t('toastsUi.nwcDisconnected'),
+ description: t('toastsUi.nwcDisconnectedDesc'),
});
};
diff --git a/src/hooks/usePlatformFee.ts b/src/hooks/usePlatformFee.ts
index e531d52..caa33ab 100644
--- a/src/hooks/usePlatformFee.ts
+++ b/src/hooks/usePlatformFee.ts
@@ -45,6 +45,14 @@ export const DURATION_TIERS: DurationTier[] = [
export const DEFAULT_TIER_ID: DurationTierId = '30d';
+/** `feesUi.*` keys for each duration tier label (single source for the UI). */
+export const DURATION_TIER_KEYS: Record = {
+ '7d': 'feesUi.duration7d',
+ '30d': 'feesUi.duration30d',
+ '90d': 'feesUi.duration90d',
+ '365d': 'feesUi.duration365d',
+};
+
/** Compute the sats price for a fee type at a given duration tier. */
export function tierPriceSats(type: FeeType, tier: DurationTier): number {
// Round to the nearest 100 sats for tidy amounts.
@@ -89,6 +97,21 @@ export const FEE_META: Record = {
+ featuredEvent: 'feesUi.feeFeaturedEventLabel',
+ labProfile: 'feesUi.feeLabProfileLabel',
+ featuredShop: 'feesUi.feeFeaturedShopLabel',
+ featuredTour: 'feesUi.feeFeaturedTourLabel',
+};
+
+export const FEE_BENEFIT_KEYS: Record = {
+ featuredEvent: 'feesUi.feeFeaturedEventBenefit',
+ labProfile: 'feesUi.feeLabProfileBenefit',
+ featuredShop: 'feesUi.feeFeaturedShopBenefit',
+ featuredTour: 'feesUi.feeFeaturedTourBenefit',
+};
+
// ─── localStorage key per fee type + account ─────────────────────────────────
// Scoped by pubkey so one account on a shared device can't inherit another's
// paid status (a device-global key let any later login skip the fee).
diff --git a/src/hooks/useStash.ts b/src/hooks/useStash.ts
index 7ec87ff..e891f60 100644
--- a/src/hooks/useStash.ts
+++ b/src/hooks/useStash.ts
@@ -5,6 +5,7 @@ import { useNostrLogin } from '@nostrify/react/login';
import { useCurrentUser } from './useCurrentUser';
import { useNostrPublish } from './useNostrPublish';
import { toast } from './useToast';
+import { useTranslation } from '@/contexts/language';
import { hasNip44 as checkNip44, isInteractiveSigner } from '@/lib/signerCapabilities';
import { cacheStashEvent, getCachedStashEvent } from '@/lib/offlineCache';
import type { NostrEvent } from '@nostrify/nostrify';
@@ -158,6 +159,7 @@ export function mergeStashes(local: Stash, remote: Stash): { stash: Stash; merge
* still resolves from any previously-cached decrypt (React Query cache).
*/
export function useStash(options?: { enabled?: boolean }) {
+ const { t } = useTranslation();
const { nostr } = useNostr();
const { user } = useCurrentUser();
const { logins } = useNostrLogin();
@@ -295,14 +297,14 @@ export function useStash(options?: { enabled?: boolean }) {
const { stash: mergedStash, merged } = mergeStashes(known, remote);
if (merged) {
toast({
- title: 'Merged changes from another device',
- description: 'Your stash was edited elsewhere — both sets of changes were kept.',
+ title: t('toastsUi.stashMerged'),
+ description: t('toastsUi.stashMergedDesc'),
});
}
return { stash: mergedStash, confirmed };
}
return { stash: remote, confirmed };
- }, [user, hasNip44, nostr, queryClient, stash]);
+ }, [user, hasNip44, nostr, queryClient, stash, t]);
/**
* Stash writes rewrite the WHOLE blob, so they must run against a just-read
diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts
index 666b582..15febed 100644
--- a/src/hooks/useZaps.ts
+++ b/src/hooks/useZaps.ts
@@ -3,6 +3,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { useRelays } from '@/lib/relays';
import { useToast } from '@/hooks/useToast';
+import { useTranslation } from '@/contexts/language';
import { useNWC } from '@/hooks/useNWCContext';
import { PAYMENT_STATUS_UNKNOWN, type NWCConnection } from '@/hooks/useNWC';
import { nip57 } from 'nostr-tools';
@@ -20,6 +21,7 @@ export function useZaps(
_nwcConnection: NWCConnection | null,
onZapSuccess?: () => void
) {
+ const { t } = useTranslation();
const { nostr } = useNostr();
const { toast } = useToast();
const { user } = useCurrentUser();
@@ -219,8 +221,8 @@ export function useZaps(
if (!user) {
toast({
- title: 'Login required',
- description: 'You must be logged in to send a zap.',
+ title: t('toastsUi.zapLoginRequired'),
+ description: t('toastsUi.zapLoginRequiredDesc'),
variant: 'destructive',
});
setIsZapping(false);
@@ -229,8 +231,8 @@ export function useZaps(
if (!actualTarget) {
toast({
- title: 'Event not found',
- description: 'Could not find the event to zap.',
+ title: t('toastsUi.zapEventNotFound'),
+ description: t('toastsUi.zapEventNotFoundDesc'),
variant: 'destructive',
});
setIsZapping(false);
@@ -240,8 +242,8 @@ export function useZaps(
try {
if (!author.data || !author.data?.metadata || !author.data?.event ) {
toast({
- title: 'Author not found',
- description: 'Could not find the author of this item.',
+ title: t('toastsUi.zapAuthorNotFound'),
+ description: t('toastsUi.zapAuthorNotFoundDesc'),
variant: 'destructive',
});
setIsZapping(false);
@@ -264,8 +266,8 @@ export function useZaps(
setInvoice(null);
toast({
- title: 'Zap successful!',
- description: `You sent ${amount} sats via NWC to the author.`,
+ title: t('toastsUi.zapSuccess'),
+ description: t('toastsUi.zapSuccessNwcDesc', { amount }),
});
// Invalidate zap queries to refresh counts
@@ -280,8 +282,8 @@ export function useZaps(
if ((nwcError as { code?: string })?.code === PAYMENT_STATUS_UNKNOWN) {
setIsZapping(false);
toast({
- title: 'Payment status unknown',
- description: 'Your wallet may still be settling this zap. Check your wallet before retrying — do not pay twice.',
+ title: t('toastsUi.zapStatusUnknown'),
+ description: t('toastsUi.zapStatusUnknownDesc'),
variant: 'destructive',
});
return;
@@ -289,10 +291,10 @@ export function useZaps(
console.error('NWC payment failed, falling back:', nwcError);
// Show specific NWC error to user for debugging
- const errorMessage = nwcError instanceof Error ? nwcError.message : 'Unknown NWC error';
+ const errorMessage = nwcError instanceof Error ? nwcError.message : t('toastsUi.zapUnknownNwcError');
toast({
- title: 'NWC payment failed',
- description: `${errorMessage}. Falling back to other payment methods...`,
+ title: t('toastsUi.zapNwcFailed'),
+ description: t('toastsUi.zapFallingBack', { error: errorMessage }),
variant: 'destructive',
});
}
@@ -319,8 +321,8 @@ export function useZaps(
setInvoice(null);
toast({
- title: 'Zap successful!',
- description: `You sent ${amount} sats to the author.`,
+ title: t('toastsUi.zapSuccess'),
+ description: t('toastsUi.zapSuccessDesc', { amount }),
});
// Invalidate zap queries to refresh counts
@@ -332,10 +334,10 @@ export function useZaps(
console.error('WebLN payment failed, falling back:', weblnError);
// Show specific WebLN error to user for debugging
- const errorMessage = weblnError instanceof Error ? weblnError.message : 'Unknown WebLN error';
+ const errorMessage = weblnError instanceof Error ? weblnError.message : t('toastsUi.zapUnknownWeblnError');
toast({
- title: 'WebLN payment failed',
- description: `${errorMessage}. Falling back to other payment methods...`,
+ title: t('toastsUi.zapWeblnFailed'),
+ description: t('toastsUi.zapFallingBack', { error: errorMessage }),
variant: 'destructive',
});
@@ -349,7 +351,7 @@ export function useZaps(
} catch (err) {
console.error('Zap error:', err);
toast({
- title: 'Zap failed',
+ title: t('toastsUi.zapFailed'),
description: (err as Error).message,
variant: 'destructive',
});
@@ -358,7 +360,7 @@ export function useZaps(
} catch (err) {
console.error('Zap error:', err);
toast({
- title: 'Zap failed',
+ title: t('toastsUi.zapFailed'),
description: (err as Error).message,
variant: 'destructive',
});
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 74c6ca3..f0d3684 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -6927,6 +6927,45 @@ walletUi: {
pleaseTryAgain: 'Please try again.',
relayUnreachable: 'Could not reach any relay — check your connection and try again.',
loginToReport: 'Log in to report a spot.',
+ zapLoginRequired: 'Login required',
+ zapLoginRequiredDesc: 'You must be logged in to send a zap.',
+ zapEventNotFound: 'Event not found',
+ zapEventNotFoundDesc: 'Could not find the event to zap.',
+ zapAuthorNotFound: 'Author not found',
+ zapAuthorNotFoundDesc: 'Could not find the author of this item.',
+ zapSuccess: 'Zap successful!',
+ zapSuccessNwcDesc: 'You sent {{amount}} sats via NWC to the author.',
+ zapSuccessDesc: 'You sent {{amount}} sats to the author.',
+ zapStatusUnknown: 'Payment status unknown',
+ zapStatusUnknownDesc: 'Your wallet may still be settling this zap. Check your wallet before retrying — do not pay twice.',
+ zapNwcFailed: 'NWC payment failed',
+ zapWeblnFailed: 'WebLN payment failed',
+ zapFallingBack: '{{error}}. Falling back to other payment methods...',
+ zapFailed: 'Zap failed',
+ zapUnknownNwcError: 'Unknown NWC error',
+ zapUnknownWeblnError: 'Unknown WebLN error',
+ nwcInvalidUri: 'Invalid NWC URI',
+ nwcInvalidUriDesc: 'Please check the connection string and try again.',
+ nwcExists: 'Connection already exists',
+ nwcExistsDesc: 'This wallet is already connected.',
+ nwcConnected: 'Wallet connected',
+ nwcConnectedDesc: 'Successfully connected to {{alias}}.',
+ nwcFailed: 'Connection failed',
+ nwcFailedDesc: 'Could not connect to the wallet: {{error}}',
+ nwcUnknownError: 'Unknown error occurred',
+ nwcDisconnected: 'Wallet disconnected',
+ nwcDisconnectedDesc: 'The wallet connection has been removed.',
+ deleteApproveAmber: 'Approve in Amber',
+ deleteApproveAmberDesc: 'Amber will open — tap Approve to confirm the deletion.',
+ deleteCancelled: 'Cancelled',
+ deleteCancelledDesc: 'You rejected the request in Amber.',
+ deleteCouldntSign: "Couldn't sign",
+ deletePublishing: 'Step 2/2: Publishing…',
+ deletePublishingDesc: 'Sending the deletion to relays.',
+ deleteStuck: 'Stuck at PUBLISHING',
+ deleteStuckDesc: "Relay step failed: {{error}}. It was signed OK; the relay didn't confirm.",
+ stashMerged: 'Merged changes from another device',
+ stashMergedDesc: 'Your stash was edited elsewhere — both sets of changes were kept.',
},
cardUi: {
save: 'Save card to Nostr',
diff --git a/src/i18n/es.ts b/src/i18n/es.ts
index 4ecdce4..004d33a 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -6927,6 +6927,45 @@ walletUi: {
pleaseTryAgain: 'Inténtalo de nuevo.',
relayUnreachable: 'No se pudo alcanzar ningún relay — revisa tu conexión e inténtalo de nuevo.',
loginToReport: 'Inicia sesión para informar de un spot.',
+ zapLoginRequired: 'Inicio de sesión requerido',
+ zapLoginRequiredDesc: 'Debes iniciar sesión para enviar un zap.',
+ zapEventNotFound: 'Evento no encontrado',
+ zapEventNotFoundDesc: 'No se pudo encontrar el evento a zapear.',
+ zapAuthorNotFound: 'Autor no encontrado',
+ zapAuthorNotFoundDesc: 'No se pudo encontrar el autor de este artículo.',
+ zapSuccess: '¡Zap enviado!',
+ zapSuccessNwcDesc: 'Enviaste {{amount}} sats vía NWC al autor.',
+ zapSuccessDesc: 'Enviaste {{amount}} sats al autor.',
+ zapStatusUnknown: 'Estado del pago desconocido',
+ zapStatusUnknownDesc: 'Es posible que tu cartera aún esté liquidando este zap. Revisa tu cartera antes de reintentar — no pagues dos veces.',
+ zapNwcFailed: 'Falló el pago por NWC',
+ zapWeblnFailed: 'Falló el pago por WebLN',
+ zapFallingBack: '{{error}}. Cambiando a otros métodos de pago...',
+ zapFailed: 'Falló el zap',
+ zapUnknownNwcError: 'Error desconocido de NWC',
+ zapUnknownWeblnError: 'Error desconocido de WebLN',
+ nwcInvalidUri: 'URI de NWC no válido',
+ nwcInvalidUriDesc: 'Revisa la cadena de conexión e inténtalo de nuevo.',
+ nwcExists: 'La conexión ya existe',
+ nwcExistsDesc: 'Esta cartera ya está conectada.',
+ nwcConnected: 'Cartera conectada',
+ nwcConnectedDesc: 'Conectado correctamente a {{alias}}.',
+ nwcFailed: 'Falló la conexión',
+ nwcFailedDesc: 'No se pudo conectar a la cartera: {{error}}',
+ nwcUnknownError: 'Se produjo un error desconocido',
+ nwcDisconnected: 'Cartera desconectada',
+ nwcDisconnectedDesc: 'Se ha eliminado la conexión de la cartera.',
+ deleteApproveAmber: 'Aprueba en Amber',
+ deleteApproveAmberDesc: 'Amber se abrirá — toca Aprobar para confirmar la eliminación.',
+ deleteCancelled: 'Cancelado',
+ deleteCancelledDesc: 'Rechazaste la solicitud en Amber.',
+ deleteCouldntSign: 'No se pudo firmar',
+ deletePublishing: 'Paso 2/2: Publicando…',
+ deletePublishingDesc: 'Enviando la eliminación a los relays.',
+ deleteStuck: 'Atascado en PUBLICANDO',
+ deleteStuckDesc: 'Falló el paso de relay: {{error}}. Se firmó bien; el relay no confirmó.',
+ stashMerged: 'Cambios fusionados desde otro dispositivo',
+ stashMergedDesc: 'Tu stash se editó en otro lugar — se conservaron ambos conjuntos de cambios.',
},
cardUi: {
save: 'Guardar carné en Nostr',