i18n(es): translate status presets, SetStatusDialog, nutzap + wallet toasts

This commit is contained in:
SkateSpots 2026-10-07 17:04:04 +07:00
parent 69458e1ae6
commit dd2062dc74
8 changed files with 151 additions and 47 deletions

View file

@ -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 */}
<div className="space-y-2">
<Label className="text-sm font-bold text-[#ccff00]">Quick Status</Label>
<Label className="text-sm font-bold text-[#ccff00]">{t('statusUi.quickStatus')}</Label>
<div className="grid grid-cols-2 gap-2">
{SKATING_STATUSES.map((status) => (
<Card
@ -189,7 +191,7 @@ export function SetStatusDialog({
<CardContent className="p-3 flex items-center gap-2">
<span className="text-xl">{status.emoji}</span>
<div className="flex-1 min-w-0">
<p className="font-bold text-sm text-white truncate">{status.label}</p>
<p className="font-bold text-sm text-white truncate">{t(status.labelKey)}</p>
</div>
{selectedStatusId === status.id && (
<div className="w-4 h-4 rounded-full bg-[#ccff00] flex items-center justify-center flex-shrink-0">

View file

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

View file

@ -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'];

View file

@ -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(() => {

View file

@ -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<string | null>(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)

View file

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

View file

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

View file

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