From 97c90c506a117c48a6b199682360bb747aac3310 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Wed, 7 Oct 2026 16:55:33 +0700 Subject: [PATCH] i18n(es): translate zap/NWC/delete/stash toasts + reuse feesUi keys for fee metadata --- src/components/ProSupportScreen.tsx | 8 +++--- src/hooks/useDeleteEntity.ts | 18 +++++++------ src/hooks/useNWC.ts | 24 +++++++++-------- src/hooks/usePlatformFee.ts | 23 ++++++++++++++++ src/hooks/useStash.ts | 8 +++--- src/hooks/useZaps.ts | 42 +++++++++++++++-------------- src/i18n/en.ts | 39 +++++++++++++++++++++++++++ src/i18n/es.ts | 39 +++++++++++++++++++++++++++ 8 files changed, 155 insertions(+), 46 deletions(-) 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',