From dd2062dc74fbb3f586e65ecf2fff5bf2dea2a1fc Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Wed, 7 Oct 2026 17:04:04 +0700 Subject: [PATCH] i18n(es): translate status presets, SetStatusDialog, nutzap + wallet toasts --- src/components/SetStatusDialog.tsx | 18 +++++---- src/hooks/useNutzap.ts | 11 ++++-- src/hooks/useUserStatus.ts | 20 +++++----- src/hooks/wallet/useLightningOperations.ts | 18 +++++---- src/hooks/wallet/useMintManager.ts | 15 +++++--- src/hooks/wallet/useTokenOperations.ts | 26 +++++++------ src/i18n/en.ts | 45 ++++++++++++++++++++++ src/i18n/es.ts | 45 ++++++++++++++++++++++ 8 files changed, 151 insertions(+), 47 deletions(-) diff --git a/src/components/SetStatusDialog.tsx b/src/components/SetStatusDialog.tsx index 05e5f73..196747b 100644 --- a/src/components/SetStatusDialog.tsx +++ b/src/components/SetStatusDialog.tsx @@ -18,6 +18,7 @@ import { useSkateSpots } from '@/hooks/useSkateSpots'; import { useUserStatus, useSetUserStatus, useClearUserStatus, SKATING_STATUSES, type SkatingStatusId } from '@/hooks/useUserStatus'; import type { SkateSpot } from '@/lib/types'; import { cn } from '@/lib/utils'; +import { useTranslation } from '@/contexts/language'; import { Loader2, MapPin, Clock, X, Zap, Search } from 'lucide-react'; interface SetStatusDialogProps { @@ -31,6 +32,7 @@ export function SetStatusDialog({ onOpenChange, preselectedSpot, }: SetStatusDialogProps) { + const { t } = useTranslation(); const { user } = useCurrentUser(); const { toast } = useToast(); const { data: currentStatus } = useUserStatus(user?.pubkey); @@ -70,8 +72,8 @@ export function SetStatusDialog({ }); toast({ - title: 'Status updated!', - description: 'Your skating status is now visible to others.', + title: t('toastsUi.statusUpdated'), + description: t('toastsUi.statusUpdatedDesc'), }); onOpenChange(false); // Reset form @@ -80,7 +82,7 @@ export function SetStatusDialog({ if (!preselectedSpot) setLinkedSpot(null); } catch (error) { toast({ - title: 'Failed to set status', + title: t('toastsUi.statusFailed'), description: error instanceof Error ? error.message : 'Something went wrong', variant: 'destructive', }); @@ -91,13 +93,13 @@ export function SetStatusDialog({ try { await clearStatus.mutateAsync(); toast({ - title: 'Status cleared', - description: 'Your status has been removed.', + title: t('toastsUi.statusCleared'), + description: t('toastsUi.statusClearedDesc'), }); onOpenChange(false); } catch (error) { toast({ - title: 'Failed to clear status', + title: t('toastsUi.statusClearFailed'), description: error instanceof Error ? error.message : 'Something went wrong', variant: 'destructive', }); @@ -170,7 +172,7 @@ export function SetStatusDialog({ {/* Quick Status Options */}
- +
{SKATING_STATUSES.map((status) => ( {status.emoji}
-

{status.label}

+

{t(status.labelKey)}

{selectedStatusId === status.id && (
diff --git a/src/hooks/useNutzap.ts b/src/hooks/useNutzap.ts index f57313c..b2cbccb 100644 --- a/src/hooks/useNutzap.ts +++ b/src/hooks/useNutzap.ts @@ -4,6 +4,7 @@ import { useCurrentUser } from './useCurrentUser'; import { useNostrPublish } from './useNostrPublish'; import { useToast } from './useToast'; +import { useTranslation } from '@/contexts/language'; import type { NostrEvent } from '@nostrify/nostrify'; // NIP-61 Kinds @@ -202,6 +203,7 @@ export function useCanReceiveNutzaps() { * Setup nutzap receiving (publish kind:10019) */ export function useSetupNutzapReceiving() { + const { t } = useTranslation(); const { user } = useCurrentUser(); const { mutateAsync: publishEvent } = useNostrPublish(); const { toast } = useToast(); @@ -246,13 +248,13 @@ export function useSetupNutzapReceiving() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['nutzap-info'] }); toast({ - title: 'Nutzap receiving enabled', - description: 'You can now receive ecash zaps!', + title: t('toastsUi.nutzapEnabled'), + description: t('toastsUi.nutzapEnabledDesc'), }); }, onError: (error) => { toast({ - title: 'Setup failed', + title: t('toastsUi.nutzapSetupFailed'), description: error instanceof Error ? error.message : 'Failed to setup nutzap receiving', variant: 'destructive', }); @@ -290,6 +292,7 @@ export const NUTZAP_SENDING_ENABLED = false; * 4. Publish kind:9321 with `proof`, `u` (mint), `p` (recipient), optional `e` */ export function useSendNutzap() { + const { t } = useTranslation(); const { user } = useCurrentUser(); const { toast } = useToast(); const queryClient = useQueryClient(); @@ -314,7 +317,7 @@ export function useSendNutzap() { }, onError: (error) => { toast({ - title: 'Nutzap sending unavailable', + title: t('toastsUi.nutzapUnavailable'), description: error instanceof Error ? error.message : 'Use a Lightning zap instead', variant: 'destructive', }); diff --git a/src/hooks/useUserStatus.ts b/src/hooks/useUserStatus.ts index 4088f8d..3d32c3c 100644 --- a/src/hooks/useUserStatus.ts +++ b/src/hooks/useUserStatus.ts @@ -7,16 +7,18 @@ import type { NostrEvent } from '@nostrify/nostrify'; // NIP-38 User Status Kind export const USER_STATUS_KIND = 30315; -// Skating status presets with emojis +// Skating status presets with emojis. `label`/`description` stay English: the +// label is published as the NIP-38 content AND matched back in parseStatusEvent, +// so it is a wire value — display uses `labelKey`/`descriptionKey` instead. export const SKATING_STATUSES = [ - { id: 'skating', label: 'Skating', emoji: '🛹', description: 'Currently skating' }, - { id: 'filming', label: 'Filming', emoji: '🎬', description: 'Filming tricks' }, - { id: 'looking-for-sesh', label: 'Looking for Sesh', emoji: '👀', description: 'Want to skate with others' }, - { id: 'coaching', label: 'Coaching', emoji: '🎓', description: 'Teaching/coaching' }, - { id: 'chilling', label: 'Chilling', emoji: '😎', description: 'Hanging out at the spot' }, - { id: 'driving-to-spot', label: 'On My Way', emoji: '🚗', description: 'Heading to a spot' }, - { id: 'taking-a-break', label: 'Taking a Break', emoji: '🧃', description: 'Resting/hydrating' }, - { id: 'done-for-today', label: 'Done for Today', emoji: '🌙', description: 'Finished skating' }, + { id: 'skating', label: 'Skating', labelKey: 'statusUi.skating', emoji: '🛹', description: 'Currently skating', descriptionKey: 'statusUi.skatingDesc' }, + { id: 'filming', label: 'Filming', labelKey: 'statusUi.filming', emoji: '🎬', description: 'Filming tricks', descriptionKey: 'statusUi.filmingDesc' }, + { id: 'looking-for-sesh', label: 'Looking for Sesh', labelKey: 'statusUi.lookingForSesh', emoji: '👀', description: 'Want to skate with others', descriptionKey: 'statusUi.lookingForSeshDesc' }, + { id: 'coaching', label: 'Coaching', labelKey: 'statusUi.coaching', emoji: '🎓', description: 'Teaching/coaching', descriptionKey: 'statusUi.coachingDesc' }, + { id: 'chilling', label: 'Chilling', labelKey: 'statusUi.chilling', emoji: '😎', description: 'Hanging out at the spot', descriptionKey: 'statusUi.chillingDesc' }, + { id: 'driving-to-spot', label: 'On My Way', labelKey: 'statusUi.onMyWay', emoji: '🚗', description: 'Heading to a spot', descriptionKey: 'statusUi.onMyWayDesc' }, + { id: 'taking-a-break', label: 'Taking a Break', labelKey: 'statusUi.takingABreak', emoji: '🧃', description: 'Resting/hydrating', descriptionKey: 'statusUi.takingABreakDesc' }, + { id: 'done-for-today', label: 'Done for Today', labelKey: 'statusUi.doneForToday', emoji: '🌙', description: 'Finished skating', descriptionKey: 'statusUi.doneForTodayDesc' }, ] as const; export type SkatingStatusId = typeof SKATING_STATUSES[number]['id']; diff --git a/src/hooks/wallet/useLightningOperations.ts b/src/hooks/wallet/useLightningOperations.ts index b8480e7..006da4c 100644 --- a/src/hooks/wallet/useLightningOperations.ts +++ b/src/hooks/wallet/useLightningOperations.ts @@ -1,6 +1,7 @@ import { useState, useCallback, useRef } from 'react'; import type { Manager } from 'coco-cashu-core'; import { useToast } from '@/hooks/useToast'; +import { useTranslation } from '@/contexts/language'; interface UseLightningOperationsProps { coco: Manager | null; @@ -13,6 +14,7 @@ export function useLightningOperations({ activeMintUrl, generateQRCode, }: UseLightningOperationsProps) { + const { t } = useTranslation(); const { toast } = useToast(); // Receive state @@ -46,8 +48,8 @@ export function useLightningOperations({ if (isNaN(amount) || amount <= 0) { toast({ variant: 'destructive', - title: 'Invalid amount', - description: 'Please enter a valid amount' + title: t('toastsUi.invalidAmount'), + description: t('toastsUi.invalidAmountDesc') }); return; } @@ -84,8 +86,8 @@ export function useLightningOperations({ if (redeemedQuote.quoteId === quoteId) { toast({ - title: 'Payment received', - description: `Successfully received ${invoiceAmount} sats!` + title: t('toastsUi.paymentReceived'), + description: t('toastsUi.paymentReceivedDesc', { amount: invoiceAmount }) }); setShowInvoiceModal(false); setCurrentInvoice(null); @@ -112,7 +114,7 @@ export function useLightningOperations({ } finally { setIsGeneratingInvoice(false); } - }, [coco, quoteAmount, activeMintUrl, generateQRCode, isGeneratingInvoice, toast]); + }, [coco, quoteAmount, activeMintUrl, generateQRCode, isGeneratingInvoice, toast, t]); // Handle send (pay Lightning invoice) const handleLightningSend = useCallback(async () => { @@ -128,8 +130,8 @@ export function useLightningOperations({ await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote); toast({ - title: 'Payment sent', - description: 'Lightning payment completed successfully' + title: t('toastsUi.paymentSent'), + description: t('toastsUi.paymentSentDesc') }); setLightningInvoice(''); @@ -147,7 +149,7 @@ export function useLightningOperations({ } finally { setIsProcessingPayment(false); } - }, [coco, lightningInvoice, activeMintUrl, isProcessingPayment, toast]); + }, [coco, lightningInvoice, activeMintUrl, isProcessingPayment, toast, t]); // Cleanup listener on unmount useCallback(() => { diff --git a/src/hooks/wallet/useMintManager.ts b/src/hooks/wallet/useMintManager.ts index 5e0c788..a211781 100644 --- a/src/hooks/wallet/useMintManager.ts +++ b/src/hooks/wallet/useMintManager.ts @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import type { Manager, Repositories } from 'coco-cashu-core'; import { useToast } from '@/hooks/useToast'; +import { useTranslation } from '@/contexts/language'; import { probeMintReachability } from '@/lib/mintReachability'; export interface CashuMint { @@ -47,6 +48,7 @@ export function useMintManager({ repositories, refreshMints, }: UseMintManagerProps) { + const { t } = useTranslation(); const { toast } = useToast(); const [activeMintUrl, setActiveMintUrl] = useState(null); const [mintUrl, setMintUrl] = useState(''); @@ -109,8 +111,8 @@ export function useMintManager({ if (!urlToAdd) { toast({ - title: 'Mint added', - description: 'Mint added and trusted successfully' + title: t('toastsUi.mintAdded'), + description: t('toastsUi.mintAddedDesc') }); } @@ -119,13 +121,13 @@ export function useMintManager({ if (!urlToAdd) { toast({ variant: 'destructive', - title: 'Failed to add mint', + title: t('toastsUi.mintAddFailed'), description: err instanceof Error ? err.message : 'Unknown error' }); } throw err; // Re-throw so external callers can handle it } - }, [mintUrl, coco, mints, toast, refreshMints]); + }, [mintUrl, coco, mints, toast, refreshMints, t]); // Show remove confirmation modal (for internal use) const handleRemoveMintClick = useCallback((mint: CashuMint) => { @@ -154,7 +156,7 @@ export function useMintManager({ } toast({ - title: 'Mint removed', + title: t('toastsUi.mintRemoved'), description: `${getCleanMintLabel(mint)} has been removed` }); @@ -162,7 +164,7 @@ export function useMintManager({ } catch (err) { toast({ variant: 'destructive', - title: 'Failed to remove mint', + title: t('toastsUi.mintRemoveFailed'), description: err instanceof Error ? err.message : 'Unknown error' }); throw err; @@ -174,6 +176,7 @@ export function useMintManager({ repositories, refreshMints, toast, + t, ]); // Confirm and remove mint (uses internal state) diff --git a/src/hooks/wallet/useTokenOperations.ts b/src/hooks/wallet/useTokenOperations.ts index 83fab37..a7f8940 100644 --- a/src/hooks/wallet/useTokenOperations.ts +++ b/src/hooks/wallet/useTokenOperations.ts @@ -3,6 +3,7 @@ import { isUnknownMintError, extractMintUrlFromError } from './useMintManager'; import type { Manager } from 'coco-cashu-core'; import type { Token } from '@cashu/cashu-ts'; import { useToast } from '@/hooks/useToast'; +import { useTranslation } from '@/contexts/language'; interface UseTokenOperationsProps { coco: Manager | null; @@ -17,6 +18,7 @@ export function useTokenOperations({ generateQRCode, getEncodedToken, }: UseTokenOperationsProps) { + const { t } = useTranslation(); const { toast } = useToast(); // Receive state @@ -69,18 +71,18 @@ export function useTokenOperations({ setTokenInput(''); toast({ - title: 'Token received', + title: t('toastsUi.tokenReceived'), description: `Token redeemed successfully! Added new mint: ${new URL(mintUrl).host}` }); } catch (retryErr) { toast({ variant: 'destructive', - title: 'Receive failed', + title: t('toastsUi.receiveFailed'), description: retryErr instanceof Error ? retryErr.message : 'Unknown error' }); } }, - [coco, tokenInput, toast] + [coco, tokenInput, toast, t] ); // Handle receive (token) @@ -96,8 +98,8 @@ export function useTokenOperations({ setTokenInput(''); toast({ - title: 'Token received', - description: 'Cashu token redeemed successfully!' + title: t('toastsUi.tokenReceived'), + description: t('toastsUi.tokenReceivedDesc') }); } catch (err) { console.error('[Cashu] Token receive error:', err); @@ -113,13 +115,13 @@ export function useTokenOperations({ toast({ variant: 'destructive', - title: 'Receive failed', + title: t('toastsUi.receiveFailed'), description: err instanceof Error ? err.message : 'Unknown error' }); } finally { setIsReceivingToken(false); } - }, [coco, tokenInput, isReceivingToken, handleUnknownMintError, toast]); + }, [coco, tokenInput, isReceivingToken, handleUnknownMintError, toast, t]); // Handle send (token) const handleTokenSend = useCallback(async () => { @@ -130,8 +132,8 @@ export function useTokenOperations({ if (isNaN(amount) || amount <= 0) { toast({ variant: 'destructive', - title: 'Invalid amount', - description: 'Please enter a valid amount' + title: t('toastsUi.invalidAmount'), + description: t('toastsUi.invalidAmountDesc') }); return; } @@ -144,19 +146,19 @@ export function useTokenOperations({ setSendAmount(''); toast({ - title: 'Token created', + title: t('toastsUi.tokenCreated'), description: `${amount} sats token ready to share` }); } catch (err) { toast({ variant: 'destructive', - title: 'Send failed', + title: t('toastsUi.sendFailed'), description: err instanceof Error ? err.message : 'Unknown error' }); } finally { setIsCreatingToken(false); } - }, [coco, sendAmount, activeMintUrl, isCreatingToken, getEncodedToken, generateSentTokenQRCode, toast]); + }, [coco, sendAmount, activeMintUrl, isCreatingToken, getEncodedToken, generateSentTokenQRCode, toast, t]); return { // Receive state diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 7b9ae42..4e2afff 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6878,6 +6878,25 @@ walletUi: { offlineViewDetails: 'View cached spot details', offlineKeepActing: 'Keep acting — posts, RSVPs, listings and messages are queued and send when you reconnect', }, + statusUi: { + quickStatus: 'Quick Status', + skating: 'Skating', + skatingDesc: 'Currently skating', + filming: 'Filming', + filmingDesc: 'Filming tricks', + lookingForSesh: 'Looking for Sesh', + lookingForSeshDesc: 'Want to skate with others', + coaching: 'Coaching', + coachingDesc: 'Teaching/coaching', + chilling: 'Chilling', + chillingDesc: 'Hanging out at the spot', + onMyWay: 'On My Way', + onMyWayDesc: 'Heading to a spot', + takingABreak: 'Taking a Break', + takingABreakDesc: 'Resting/hydrating', + doneForToday: 'Done for Today', + doneForTodayDesc: 'Finished skating', + }, sasquatchGuide: { homeTitle: 'Welcome to SkateSpots! 🦧', homeMessage: "Yo, I'm The Sasquatch. Keeper of boards, trails, and wild rides. Welcome to the untamed skate world on Nostr—no suits, no ads, just skaters carving their own path. Here, you own your tricks, your data, and even earn Bitcoin for leaving your mark. Connect with skaters in your hood or across the globe. No rules but shredding, no footprints but your own.", @@ -7011,6 +7030,32 @@ walletUi: { 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.', + statusUpdated: 'Status updated!', + statusUpdatedDesc: 'Your skating status is now visible to others.', + statusFailed: 'Failed to set status', + statusCleared: 'Status cleared', + statusClearedDesc: 'Your status has been removed.', + statusClearFailed: 'Failed to clear status', + nutzapEnabled: 'Nutzap receiving enabled', + nutzapEnabledDesc: 'You can now receive ecash zaps!', + nutzapSetupFailed: 'Setup failed', + nutzapUnavailable: 'Nutzap sending unavailable', + tokenReceived: 'Token received', + tokenReceivedDesc: 'Cashu token redeemed successfully!', + receiveFailed: 'Receive failed', + invalidAmount: 'Invalid amount', + invalidAmountDesc: 'Please enter a valid amount', + tokenCreated: 'Token created', + sendFailed: 'Send failed', + mintAdded: 'Mint added', + mintAddedDesc: 'Mint added and trusted successfully', + mintAddFailed: 'Failed to add mint', + mintRemoved: 'Mint removed', + mintRemoveFailed: 'Failed to remove mint', + paymentReceived: 'Payment received', + paymentReceivedDesc: 'Successfully received {{amount}} sats!', + paymentSent: 'Payment sent', + paymentSentDesc: 'Lightning payment completed successfully', }, cardUi: { save: 'Save card to Nostr', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 7038101..d66e437 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6878,6 +6878,25 @@ walletUi: { offlineViewDetails: 'Ver detalles de spots en caché', offlineKeepActing: 'Sigue actuando — publicaciones, RSVPs, anuncios y mensajes se ponen en cola y se envían al reconectar', }, + statusUi: { + quickStatus: 'Estado rápido', + skating: 'Patinando', + skatingDesc: 'Patinando ahora mismo', + filming: 'Grabando', + filmingDesc: 'Grabando trucos', + lookingForSesh: 'Buscando sesión', + lookingForSeshDesc: 'Quiere patinar con otros', + coaching: 'Dando clase', + coachingDesc: 'Enseñando/dando clase', + chilling: 'De chill', + chillingDesc: 'Pasando el rato en el spot', + onMyWay: 'De camino', + onMyWayDesc: 'Yendo a un spot', + takingABreak: 'Descansando', + takingABreakDesc: 'Descansando/hidratándose', + doneForToday: 'Por hoy ya', + doneForTodayDesc: 'Terminó de patinar', + }, sasquatchGuide: { homeTitle: '¡Bienvenido a SkateSpots! 🦧', homeMessage: 'Yo, soy The Sasquatch. Guardián de tablas, rutas y aventuras salvajes. Bienvenido al indómito mundo del skate en Nostr — sin trajes, sin anuncios, solo skaters abriendo su propio camino. Aquí tus trucos y tus datos son tuyos, y hasta ganas Bitcoin por dejar tu huella. Conecta con skaters de tu barrio o de todo el mundo. Sin más reglas que rodar, sin más huellas que las tuyas.', @@ -7011,6 +7030,32 @@ walletUi: { 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.', + statusUpdated: '¡Estado actualizado!', + statusUpdatedDesc: 'Tu estado de skate ahora es visible para los demás.', + statusFailed: 'No se pudo establecer el estado', + statusCleared: 'Estado borrado', + statusClearedDesc: 'Tu estado se ha eliminado.', + statusClearFailed: 'No se pudo borrar el estado', + nutzapEnabled: 'Recepción de Nutzap activada', + nutzapEnabledDesc: '¡Ya puedes recibir zaps ecash!', + nutzapSetupFailed: 'Falló la configuración', + nutzapUnavailable: 'Envío de Nutzap no disponible', + tokenReceived: 'Token recibido', + tokenReceivedDesc: '¡Token Cashu canjeado con éxito!', + receiveFailed: 'Falló la recepción', + invalidAmount: 'Importe no válido', + invalidAmountDesc: 'Introduce un importe válido', + tokenCreated: 'Token creado', + sendFailed: 'Falló el envío', + mintAdded: 'Mint añadido', + mintAddedDesc: 'Mint añadido y confiado correctamente', + mintAddFailed: 'No se pudo añadir el mint', + mintRemoved: 'Mint eliminado', + mintRemoveFailed: 'No se pudo eliminar el mint', + paymentReceived: 'Pago recibido', + paymentReceivedDesc: '¡Recibidos {{amount}} sats con éxito!', + paymentSent: 'Pago enviado', + paymentSentDesc: 'Pago Lightning completado correctamente', }, cardUi: { save: 'Guardar carné en Nostr',