i18n(es): translate zap/NWC/delete/stash toasts + reuse feesUi keys for fee metadata

This commit is contained in:
SkateSpots 2026-10-07 16:55:33 +07:00
parent b01bd22e96
commit 97c90c506a
8 changed files with 155 additions and 46 deletions

View file

@ -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) => (
<div key={type}>
<p className="text-xs font-bold text-gray-300">
<span className="mr-1">{FEE_META[type].emoji}</span>{FEE_META[type].label}
<span className="ml-2 font-normal text-muted-foreground">{FEE_META[type].benefit}</span>
<span className="mr-1">{FEE_META[type].emoji}</span>{t(FEE_LABEL_KEYS[type])}
<span className="ml-2 font-normal text-muted-foreground">{t(FEE_BENEFIT_KEYS[type])}</span>
</p>
<div className="mt-1.5 grid grid-cols-2 gap-1.5 sm:grid-cols-4">
{DURATION_TIERS.map((tier) => (
<div key={tier.id} className={cn('rounded-lg border p-2 text-center', tier.bestValue ? 'border-[#ccff00]/40 bg-[#ccff00]/5' : 'border-white/10')}>
<p className="text-[11px] font-bold text-white">{tier.label}</p>
<p className="text-[11px] font-bold text-white">{t(DURATION_TIER_KEYS[tier.id])}</p>
<p className="text-xs font-black text-white">{tierPriceSats(type, tier).toLocaleString()}</p>
<p className="text-[10px] text-muted-foreground">{tierPerDaySats(type, tier).toLocaleString()}/{t('settingsUi.proSupport.day')}</p>
</div>

View file

@ -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<T>(promise: Promise<T>, 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}`);

View file

@ -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<NWCConnection[]>('nwc-connections', []);
const [activeConnection, setActiveConnection] = useLocalStorage<string | null>('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'),
});
};

View file

@ -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<DurationTierId, string> = {
'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<FeeType, {
},
};
/** `feesUi.*` keys for each fee type's label/benefit (UI translates these). */
export const FEE_LABEL_KEYS: Record<FeeType, string> = {
featuredEvent: 'feesUi.feeFeaturedEventLabel',
labProfile: 'feesUi.feeLabProfileLabel',
featuredShop: 'feesUi.feeFeaturedShopLabel',
featuredTour: 'feesUi.feeFeaturedTourLabel',
};
export const FEE_BENEFIT_KEYS: Record<FeeType, string> = {
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).

View file

@ -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

View file

@ -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',
});

View file

@ -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',

View file

@ -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',