- Support, not a purchase — it buys nothing and
- there are no refunds. Merchandise and Sasquatch Pro are separate. Your location and
- identity are never involved in a quiet payment.
+ {t('supportUi.notAPurchase')}
>
)}
diff --git a/src/hooks/useFollowing.ts b/src/hooks/useFollowing.ts
index a1a954e..794ec22 100644
--- a/src/hooks/useFollowing.ts
+++ b/src/hooks/useFollowing.ts
@@ -2,6 +2,7 @@ import { useNostr } from '@nostrify/react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useCurrentUser } from './useCurrentUser';
import { useToast } from './useToast';
+import { useTranslation } from '@/contexts/language';
import { nip19 } from 'nostr-tools';
/**
@@ -74,6 +75,7 @@ export function useFollowers(pubkey?: string) {
* Hook to follow/unfollow users
*/
export function useFollowActions() {
+ const { t } = useTranslation();
const { nostr } = useNostr();
const { user } = useCurrentUser();
const { toast } = useToast();
@@ -120,13 +122,13 @@ export function useFollowActions() {
queryClient.invalidateQueries({ queryKey: ['trust-score'] });
toast({
- title: 'Followed successfully',
- description: 'This user is now in your network',
+ title: t('toastsUi.followingSuccess'),
+ description: t('toastsUi.followingSuccessDesc'),
});
},
onError: (error) => {
toast({
- title: 'Failed to follow',
+ title: t('toastsUi.followFailed'),
description: error instanceof Error ? error.message : 'Unknown error',
variant: 'destructive',
});
@@ -169,13 +171,13 @@ export function useFollowActions() {
queryClient.invalidateQueries({ queryKey: ['trust-score'] });
toast({
- title: 'Unfollowed successfully',
- description: 'Removed from your network',
+ title: t('toastsUi.unfollowSuccess'),
+ description: t('toastsUi.unfollowSuccessDesc'),
});
},
onError: (error) => {
toast({
- title: 'Failed to unfollow',
+ title: t('toastsUi.unfollowFailed'),
description: error instanceof Error ? error.message : 'Unknown error',
variant: 'destructive',
});
@@ -217,10 +219,10 @@ export function useFollowActions() {
return;
}
- throw new Error('Invalid identifier. Use npub1..., user@domain.com, or hex pubkey');
+ throw new Error(t('toastsUi.invalidIdentifier'));
} catch (error) {
toast({
- title: 'Failed to follow',
+ title: t('toastsUi.followFailed'),
description: error instanceof Error ? error.message : 'Invalid identifier',
variant: 'destructive',
});
diff --git a/src/hooks/useNostrPublish.ts b/src/hooks/useNostrPublish.ts
index 26356f1..cc662a6 100644
--- a/src/hooks/useNostrPublish.ts
+++ b/src/hooks/useNostrPublish.ts
@@ -5,6 +5,7 @@ import { useCurrentUser } from "./useCurrentUser";
import { isOffline, queueSignedEvent } from "@/lib/offlineCache";
import { minePow } from "@/lib/pow";
import { toast } from "@/hooks/useToast";
+import { useTranslation } from "@/contexts/language";
import type { NostrEvent, NPool } from "@nostrify/nostrify";
@@ -13,12 +14,12 @@ import type { NostrEvent, NPool } from "@nostrify/nostrify";
* happened (above), so an interactive signer is NOT prompted again later — we
* just hand the signature over.
*/
-async function queueForLater(event: NostrEvent): Promise {
+async function queueForLater(event: NostrEvent, tr: (key: string) => string): Promise {
try {
await queueSignedEvent(event);
toast({
- title: 'Saved offline',
- description: "We'll publish it automatically when you're back online.",
+ title: tr('toastsUi.savedOffline'),
+ description: tr('toastsUi.savedOfflineDesc'),
});
return true;
} catch {
@@ -36,6 +37,7 @@ async function queueForLater(event: NostrEvent): Promise {
export function useNostrPublish(): UseMutationResult {
const { nostr } = useNostr();
const { user } = useCurrentUser();
+ const { t: tr } = useTranslation();
return useMutation({
mutationFn: async (t: Omit & { pow?: number }) => {
@@ -73,7 +75,7 @@ export function useNostrPublish(): UseMutationResult {
// Offline: don't sit through a 15s publish timeout before giving up.
if (isOffline()) {
- await queueForLater(event);
+ await queueForLater(event, tr);
return event;
}
@@ -82,7 +84,7 @@ export function useNostrPublish(): UseMutationResult {
} catch (err) {
// Relays unreachable — keep it and deliver later, unless queuing failed
// (then the real error is the honest thing to surface).
- const queued = await queueForLater(event);
+ const queued = await queueForLater(event, tr);
if (!queued) throw err;
}
diff --git a/src/hooks/useNotASpotReport.ts b/src/hooks/useNotASpotReport.ts
index 5479af7..c6aa2bc 100644
--- a/src/hooks/useNotASpotReport.ts
+++ b/src/hooks/useNotASpotReport.ts
@@ -3,6 +3,7 @@ import { useNostr } from '@nostrify/react';
import { useSkateRelays } from '@/hooks/useSkateRelays';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
+import { useTranslation } from '@/contexts/language';
import { MODERATION_LABEL_NAMESPACE } from '@/lib/types';
/**
@@ -18,6 +19,7 @@ import { MODERATION_LABEL_NAMESPACE } from '@/lib/types';
* not for disagreeing about whether a spot is good.
*/
export function useNotASpotReport() {
+ const { t } = useTranslation();
const { user } = useCurrentUser();
const { nostr } = useNostr();
const { getWriteRelays } = useSkateRelays();
@@ -35,7 +37,7 @@ export function useNotASpotReport() {
pubkey: string;
title?: string;
}) => {
- if (!user) throw new Error('Log in to report a spot.');
+ if (!user) throw new Error(t('toastsUi.loginToReport'));
const event = await user.signer.signEvent({
kind: 1984, // NIP-56 report
@@ -60,20 +62,19 @@ export function useNotASpotReport() {
),
);
if (results.every((r) => r.status === 'rejected')) {
- throw new Error('Could not reach any relay — check your connection and try again.');
+ throw new Error(t('toastsUi.relayUnreachable'));
}
},
onSuccess: () => {
toast({
- title: 'Thanks — sent for review',
- description:
- 'No hard feelings: this flags the pin as off-topic so people can decide for themselves.',
+ title: t('toastsUi.notASpotThanks'),
+ description: t('toastsUi.notASpotThanksDesc'),
});
},
onError: (error) => {
toast({
- title: 'Could not send the report',
- description: error instanceof Error ? error.message : 'Please try again.',
+ title: t('toastsUi.notASpotFailed'),
+ description: error instanceof Error ? error.message : t('toastsUi.pleaseTryAgain'),
variant: 'destructive',
});
},
diff --git a/src/hooks/useOfflineCache.ts b/src/hooks/useOfflineCache.ts
index d5e1fd4..83520d1 100644
--- a/src/hooks/useOfflineCache.ts
+++ b/src/hooks/useOfflineCache.ts
@@ -22,6 +22,7 @@ import {
} from '@/lib/offlineCache';
import { useCurrentUser } from './useCurrentUser';
import { useToast } from './useToast';
+import { useTranslation } from '@/contexts/language';
import type { NostrEvent } from '@nostrify/nostrify';
import type { SkateSpot } from '@/lib/types';
@@ -121,6 +122,7 @@ export function useCacheStats() {
* Hook to manage cache (clear old/all)
*/
export function useCacheManagement() {
+ const { t } = useTranslation();
const queryClient = useQueryClient();
const { toast } = useToast();
@@ -133,8 +135,8 @@ export function useCacheManagement() {
queryClient.invalidateQueries({ queryKey: ['cached-spots'] });
queryClient.invalidateQueries({ queryKey: ['cache-stats'] });
toast({
- title: 'Cache cleaned',
- description: `Removed ${count} old spots from cache`,
+ title: t('toastsUi.cacheCleaned'),
+ description: t('toastsUi.cacheCleanedDesc', { count }),
});
},
});
@@ -145,8 +147,8 @@ export function useCacheManagement() {
queryClient.invalidateQueries({ queryKey: ['cached-spots'] });
queryClient.invalidateQueries({ queryKey: ['cache-stats'] });
toast({
- title: 'Cache cleared',
- description: 'All cached spots have been removed',
+ title: t('toastsUi.cacheCleared'),
+ description: t('toastsUi.cacheClearedDesc'),
});
},
});
@@ -184,6 +186,7 @@ export function usePendingPostsCount() {
* through `useNostrPublish` — that would re-queue on failure and duplicate.
*/
export function useSyncPendingPosts() {
+ const { t } = useTranslation();
const isOnline = useOnlineStatus();
const queryClient = useQueryClient();
const { nostr } = useNostr();
@@ -240,8 +243,8 @@ export function useSyncPendingPosts() {
if (successCount > 0) {
toast({
- title: 'Back online',
- description: `Sent ${successCount} saved action${successCount > 1 ? 's' : ''}.`,
+ title: t('toastsUi.backOnline'),
+ description: t('toastsUi.backOnlineDesc', { count: successCount }),
});
}
};
@@ -254,7 +257,7 @@ export function useSyncPendingPosts() {
clearTimeout(timeout);
clearInterval(interval);
};
- }, [isOnline, user, nostr, queryClient, toast]);
+ }, [isOnline, user, nostr, queryClient, toast, t]);
return { isSyncing };
}
@@ -263,6 +266,7 @@ export function useSyncPendingPosts() {
* Hook to queue a post for offline publishing
*/
export function useQueuePost() {
+ const { t } = useTranslation();
const queryClient = useQueryClient();
const { toast } = useToast();
@@ -273,8 +277,8 @@ export function useQueuePost() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['pending-posts-count'] });
toast({
- title: 'Post queued',
- description: 'Your post will be published when you\'re back online',
+ title: t('toastsUi.postQueued'),
+ description: t('toastsUi.postQueuedDesc'),
});
},
});
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index f0cbad8..74c6ca3 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -6878,6 +6878,56 @@ walletUi: {
offlineViewDetails: 'View cached spot details',
offlineKeepActing: 'Keep acting — posts, RSVPs, listings and messages are queued and send when you reconnect',
},
+ supportUi: {
+ keepMapFree: 'Keep the map free',
+ keepMapFreeDesc: 'No ads, no tracking, no VC. Your sats pay for hosting, the AI node and the relay.',
+ thankYou: 'Thank you! ⚡',
+ satsKeepMapFree: 'Your sats keep the map free.',
+ keptFreeNext: 'You just kept SkateSpots free for the next skater.',
+ scanToPay: 'Scan to pay',
+ zapSkateSpots: 'Zap SkateSpots',
+ payQuietly: 'Pay quietly',
+ pickAmount: 'Pick an amount',
+ enterAmount: 'Enter how many sats you want to send.',
+ couldNotStartPayment: 'Could not start the payment.',
+ customAmountAria: 'Custom amount in sats',
+ commentPlaceholder: 'Say something (optional — public zaps only)',
+ publicZapTitle: 'Public zap with your comment',
+ loginToZapTitle: 'Log in to send a public zap',
+ anonymousTitle: 'Anonymous — no login, no record',
+ copyLightningTitle: 'Copy the lightning address',
+ copiedSuffix: '{{label}} copied',
+ invoiceLabel: 'Invoice',
+ addressLabel: 'Address',
+ notAPurchase: 'Support, not a purchase — it buys nothing and there are no refunds. Merchandise and Sasquatch Pro are separate. Your location and identity are never involved in a quiet payment.',
+ },
+ toastsUi: {
+ followingSuccess: 'Followed successfully',
+ followingSuccessDesc: 'This user is now in your network',
+ followFailed: 'Failed to follow',
+ unfollowSuccess: 'Unfollowed successfully',
+ unfollowSuccessDesc: 'Removed from your network',
+ unfollowFailed: 'Failed to unfollow',
+ reportThanks: 'Thanks — sent for review',
+ reportFailed: 'Could not send the report',
+ savedOffline: 'Saved offline',
+ savedOfflineDesc: "We'll publish it automatically when you're back online.",
+ cacheCleaned: 'Cache cleaned',
+ cacheCleanedDesc: 'Removed {{count}} old spots from cache',
+ cacheCleared: 'Cache cleared',
+ cacheClearedDesc: 'All cached spots have been removed',
+ backOnline: 'Back online',
+ backOnlineDesc: 'Sent {{count}} saved action(s).',
+ postQueued: 'Post queued',
+ postQueuedDesc: "Your post will be published when you're back online",
+ invalidIdentifier: 'Invalid identifier. Use npub1..., user@domain.com, or hex pubkey',
+ notASpotThanks: 'Thanks — sent for review',
+ notASpotThanksDesc: 'No hard feelings: this flags the pin as off-topic so people can decide for themselves.',
+ notASpotFailed: 'Could not send the report',
+ pleaseTryAgain: 'Please try again.',
+ relayUnreachable: 'Could not reach any relay — check your connection and try again.',
+ loginToReport: 'Log in to report a spot.',
+ },
cardUi: {
save: 'Save card to Nostr',
saved: 'Card saved',
diff --git a/src/i18n/es.ts b/src/i18n/es.ts
index de81350..4ecdce4 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -6878,6 +6878,56 @@ 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',
},
+ supportUi: {
+ keepMapFree: 'Mantén el mapa gratis',
+ keepMapFreeDesc: 'Sin anuncios, sin rastreo, sin capital riesgo. Tus sats pagan el hosting, el nodo de IA y el relay.',
+ thankYou: '¡Gracias! ⚡',
+ satsKeepMapFree: 'Tus sats mantienen el mapa gratis.',
+ keptFreeNext: 'Acabas de mantener SkateSpots gratis para el próximo skater.',
+ scanToPay: 'Escanea para pagar',
+ zapSkateSpots: 'Zapear a SkateSpots',
+ payQuietly: 'Pagar en privado',
+ pickAmount: 'Elige un importe',
+ enterAmount: 'Introduce cuántos sats quieres enviar.',
+ couldNotStartPayment: 'No se pudo iniciar el pago.',
+ customAmountAria: 'Importe personalizado en sats',
+ commentPlaceholder: 'Di algo (opcional — solo zaps públicos)',
+ publicZapTitle: 'Zap público con tu comentario',
+ loginToZapTitle: 'Inicia sesión para enviar un zap público',
+ anonymousTitle: 'Anónimo — sin login, sin registro',
+ copyLightningTitle: 'Copiar la dirección Lightning',
+ copiedSuffix: '{{label}} copiado',
+ invoiceLabel: 'Factura',
+ addressLabel: 'Dirección',
+ notAPurchase: 'Es apoyo, no una compra — no compra nada y no hay reembolsos. La merch y Sasquatch Pro son aparte. Tu ubicación e identidad nunca intervienen en un pago privado.',
+ },
+ toastsUi: {
+ followingSuccess: 'Siguiendo',
+ followingSuccessDesc: 'Este usuario ya está en tu red',
+ followFailed: 'No se pudo seguir',
+ unfollowSuccess: 'Dejaste de seguir',
+ unfollowSuccessDesc: 'Eliminado de tu red',
+ unfollowFailed: 'No se pudo dejar de seguir',
+ reportThanks: 'Gracias — enviado a revisión',
+ reportFailed: 'No se pudo enviar el informe',
+ savedOffline: 'Guardado sin conexión',
+ savedOfflineDesc: 'Lo publicaremos automáticamente al volver a estar en línea.',
+ cacheCleaned: 'Caché limpiada',
+ cacheCleanedDesc: 'Se eliminaron {{count}} spots antiguos de la caché',
+ cacheCleared: 'Caché borrada',
+ cacheClearedDesc: 'Se han eliminado todos los spots en caché',
+ backOnline: 'De nuevo en línea',
+ backOnlineDesc: 'Se enviaron {{count}} acción(es) guardadas.',
+ postQueued: 'Publicación en cola',
+ postQueuedDesc: 'Tu publicación se enviará cuando vuelvas a estar en línea',
+ invalidIdentifier: 'Identificador no válido. Usa npub1..., usuario@dominio.com o clave hex',
+ notASpotThanks: 'Gracias — enviado a revisión',
+ notASpotThanksDesc: 'Sin rencores: esto marca el pin como fuera de tema para que cada cual decida.',
+ notASpotFailed: 'No se pudo enviar el informe',
+ 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.',
+ },
cardUi: {
save: 'Guardar carné en Nostr',
saved: 'Carné guardado',
diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx
index cfeff66..9c7d1c1 100644
--- a/src/pages/NIP19Page.tsx
+++ b/src/pages/NIP19Page.tsx
@@ -669,9 +669,9 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
{g.forSale && (
{g.willingToTrade && !g.priceSats ? (
- <>Trade>
+ <>{t('marketUi.seller.trade')}>
) : (
- <>For Sale>
+ <>{t('marketUi.gearSwap.forSale')}>
)}
)}