i18n(es): translate SupportSkateSpots + hook toasts (follow, report, offline, publish)
This commit is contained in:
parent
7e578fb7d9
commit
b01bd22e96
8 changed files with 167 additions and 58 deletions
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from '@/lib/lightningAddress';
|
||||
import { Zap, Copy, Check, Loader2, CheckCircle2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
const PLATFORM_PUBKEY = PLATFORM_OWNER_PUBKEYS[0];
|
||||
const PRESETS = [1000, 5000, 21000];
|
||||
|
|
@ -39,6 +40,7 @@ const PRESETS = [1000, 5000, 21000];
|
|||
type PayState = 'idle' | 'preparing' | 'awaiting' | 'paid' | 'error';
|
||||
|
||||
export function SupportSkateSpots({ className }: { className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useCurrentUser();
|
||||
const { getActiveConnection, sendPayment } = useNWC();
|
||||
const { webln } = useWallet();
|
||||
|
|
@ -105,7 +107,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
|
||||
const pay = async (mode: 'zap' | 'quiet') => {
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
toast({ title: 'Pick an amount', description: 'Enter how many sats you want to send.', variant: 'destructive' });
|
||||
toast({ title: t('supportUi.pickAmount'), description: t('supportUi.enterAmount'), variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
setState('preparing');
|
||||
|
|
@ -119,7 +121,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
try {
|
||||
await sendPayment(nwc, pr);
|
||||
setState('paid');
|
||||
toast({ title: 'Thank you! ⚡', description: 'Your sats keep the map free.' });
|
||||
toast({ title: t('supportUi.thankYou'), description: t('supportUi.satsKeepMapFree') });
|
||||
return;
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
|
@ -128,7 +130,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
if (typeof webln.enable === 'function') await webln.enable();
|
||||
await webln.sendPayment(pr);
|
||||
setState('paid');
|
||||
toast({ title: 'Thank you! ⚡', description: 'Your sats keep the map free.' });
|
||||
toast({ title: t('supportUi.thankYou'), description: t('supportUi.satsKeepMapFree') });
|
||||
return;
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
|
@ -136,7 +138,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
setState('awaiting');
|
||||
} catch (err) {
|
||||
setState('error');
|
||||
setError(err instanceof Error ? err.message : 'Could not start the payment.');
|
||||
setError(err instanceof Error ? err.message : t('supportUi.couldNotStartPayment'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -144,7 +146,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
toast({ title: `${label} copied` });
|
||||
toast({ title: t('supportUi.copiedSuffix', { label }) });
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch { /* clipboard blocked */ }
|
||||
};
|
||||
|
|
@ -159,34 +161,34 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
<Zap className="h-5 w-5" />
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="text-lg font-black uppercase text-white leading-tight">Keep the map free</h3>
|
||||
<p className="text-xs text-gray-400">No ads, no tracking, no VC. Your sats pay for hosting, the AI node and the relay.</p>
|
||||
<h3 className="text-lg font-black uppercase text-white leading-tight">{t('supportUi.keepMapFree')}</h3>
|
||||
<p className="text-xs text-gray-400">{t('supportUi.keepMapFreeDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state === 'paid' ? (
|
||||
<div className="rounded-xl border border-[#ccff00]/30 bg-[#ccff00]/10 p-4 text-center space-y-2">
|
||||
<CheckCircle2 className="mx-auto h-7 w-7 text-[#ccff00]" />
|
||||
<p className="font-black text-[#ccff00]">Thank you! ⚡</p>
|
||||
<p className="text-xs text-gray-300">You just kept SkateSpots free for the next skater.</p>
|
||||
<Button size="sm" variant="outline" className="border-white/15" onClick={reset}>Done</Button>
|
||||
<p className="font-black text-[#ccff00]">{t('supportUi.thankYou')}</p>
|
||||
<p className="text-xs text-gray-300">{t('supportUi.keptFreeNext')}</p>
|
||||
<Button size="sm" variant="outline" className="border-white/15" onClick={reset}>{t('common.done')}</Button>
|
||||
</div>
|
||||
) : state === 'awaiting' && qr ? (
|
||||
<div className="space-y-3 text-center">
|
||||
<p className="text-sm text-gray-300">
|
||||
Scan to pay <span className="font-black text-white">{amount.toLocaleString()} sats</span>
|
||||
{t('supportUi.scanToPay')} <span className="font-black text-white">{amount.toLocaleString()} sats</span>
|
||||
</p>
|
||||
<img src={qr} alt="Lightning invoice QR" className="mx-auto rounded-xl bg-white p-2" width={240} height={240} />
|
||||
<img src={qr} alt={t('feesUi.invoiceQrAlt')} className="mx-auto rounded-xl bg-white p-2" width={240} height={240} />
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1 gap-2 border-white/15" onClick={() => copy(invoice ?? '', 'Invoice')}>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} Copy invoice
|
||||
<Button variant="outline" className="flex-1 gap-2 border-white/15" onClick={() => copy(invoice ?? '', t('supportUi.invoiceLabel'))}>
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} {t('walletUi.nwc.copyInvoice')}
|
||||
</Button>
|
||||
<Button className="flex-1 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90" onClick={reset}>
|
||||
Done
|
||||
{t('common.done')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Pays to <span className="font-mono text-gray-300">⚡ {PLATFORM_LIGHTNING_ADDRESS}</span>
|
||||
{t('sasquatchUi.paysTo')} <span className="font-mono text-gray-300">⚡ {PLATFORM_LIGHTNING_ADDRESS}</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -213,7 +215,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
value={amount}
|
||||
onChange={(e) => setAmount(Number(e.target.value))}
|
||||
className="h-9 w-28 border-white/15 bg-black/40 text-sm text-white"
|
||||
aria-label="Custom amount in sats"
|
||||
aria-label={t('supportUi.customAmountAria')}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">sats</span>
|
||||
</div>
|
||||
|
|
@ -222,7 +224,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
<Input
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder="Say something (optional — public zaps only)"
|
||||
placeholder={t('supportUi.commentPlaceholder')}
|
||||
maxLength={120}
|
||||
className="h-9 border-white/15 bg-black/40 text-sm text-white placeholder:text-gray-600"
|
||||
/>
|
||||
|
|
@ -233,28 +235,28 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
<Button
|
||||
onClick={() => pay('zap')}
|
||||
disabled={state === 'preparing' || !user}
|
||||
title={user ? 'Public zap with your comment' : 'Log in to send a public zap'}
|
||||
title={user ? t('supportUi.publicZapTitle') : t('supportUi.loginToZapTitle')}
|
||||
className="flex-1 gap-2 bg-[#ccff00] font-black uppercase text-black hover:bg-[#ccff00]/90"
|
||||
>
|
||||
{state === 'preparing' ? <Loader2 className="h-4 w-4 animate-spin" /> : <Zap className="h-4 w-4" />}
|
||||
Zap SkateSpots
|
||||
{t('supportUi.zapSkateSpots')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => pay('quiet')}
|
||||
disabled={state === 'preparing'}
|
||||
title="Anonymous — no login, no record"
|
||||
title={t('supportUi.anonymousTitle')}
|
||||
variant="outline"
|
||||
className="flex-1 border-white/20 text-gray-200 hover:bg-white/5"
|
||||
>
|
||||
Pay quietly
|
||||
{t('supportUi.payQuietly')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<button
|
||||
onClick={() => copy(PLATFORM_LIGHTNING_ADDRESS, 'Address')}
|
||||
onClick={() => copy(PLATFORM_LIGHTNING_ADDRESS, t('supportUi.addressLabel'))}
|
||||
className="flex items-center gap-1.5 font-mono text-[11px] text-gray-400 hover:text-[#ccff00] transition-colors"
|
||||
title="Copy the lightning address"
|
||||
title={t('supportUi.copyLightningTitle')}
|
||||
>
|
||||
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||
⚡ {PLATFORM_LIGHTNING_ADDRESS}
|
||||
|
|
@ -262,9 +264,7 @@ export function SupportSkateSpots({ className }: { className?: string }) {
|
|||
</div>
|
||||
|
||||
<p className="text-[11px] leading-relaxed text-gray-600">
|
||||
Support, <strong className="text-gray-500">not a purchase</strong> — 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')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
async function queueForLater(event: NostrEvent, tr: (key: string) => string): Promise<boolean> {
|
||||
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<boolean> {
|
|||
export function useNostrPublish(): UseMutationResult<NostrEvent> {
|
||||
const { nostr } = useNostr();
|
||||
const { user } = useCurrentUser();
|
||||
const { t: tr } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (t: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'> & { pow?: number }) => {
|
||||
|
|
@ -73,7 +75,7 @@ export function useNostrPublish(): UseMutationResult<NostrEvent> {
|
|||
|
||||
// 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<NostrEvent> {
|
|||
} 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -669,9 +669,9 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
{g.forSale && (
|
||||
<span className="absolute top-2 left-2 flex items-center gap-1 text-[10px] font-black uppercase tracking-wide px-2 py-0.5 rounded-full bg-[#ccff00] text-black">
|
||||
{g.willingToTrade && !g.priceSats ? (
|
||||
<><ArrowRightLeft className="h-2.5 w-2.5" />Trade</>
|
||||
<><ArrowRightLeft className="h-2.5 w-2.5" />{t('marketUi.seller.trade')}</>
|
||||
) : (
|
||||
<><Tag className="h-2.5 w-2.5" />For Sale</>
|
||||
<><Tag className="h-2.5 w-2.5" />{t('marketUi.gearSwap.forSale')}</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue