i18n(es): translate page/component toasts (follow, delete, reports, sync, review queue)

This commit is contained in:
SkateSpots 2026-10-07 17:14:18 +07:00
parent 2e9cbfba2b
commit 94a6628faa
7 changed files with 83 additions and 33 deletions

View file

@ -13,6 +13,7 @@ import {
import { useDeleteEntity } from '@/hooks/useDeleteEntity';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
import { useTranslation } from '@/contexts/language';
import { isAdminPubkey } from '@/lib/admins';
import { cn } from '@/lib/utils';
import type { NostrEvent } from '@nostrify/nostrify';
@ -60,6 +61,7 @@ export function DeleteEntityDialog({
}: DeleteEntityDialogProps) {
const { user } = useCurrentUser();
const { mutateAsync: deleteEntity, isPending } = useDeleteEntity();
const { t } = useTranslation();
const { toast } = useToast();
const navigate = useNavigate();
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
@ -98,8 +100,8 @@ export function DeleteEntityDialog({
if (redirectTo) navigate(redirectTo);
} catch (error) {
toast({
title: 'Delete failed',
description: error instanceof Error ? error.message : 'Something went wrong',
title: t('toastsUi.deleteFailed'),
description: error instanceof Error ? error.message : t('toastsUi.somethingWentWrong'),
variant: 'destructive',
});
}

View file

@ -3,6 +3,7 @@ import { useFollowActions, useIsFollowing } from '@/hooks/useFollowing';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { useToast } from '@/hooks/useToast';
import { useTranslation } from '@/contexts/language';
import { genUserName } from '@/lib/genUserName';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
@ -23,6 +24,7 @@ interface FounderFollowProps {
}
export function FounderFollow({ className, size = 'md', showBadge = true }: FounderFollowProps) {
const { t } = useTranslation();
const { user } = useCurrentUser();
const { follow } = useFollowActions();
const isFollowing = useIsFollowing(FOUNDER_PUBKEY);
@ -66,8 +68,8 @@ export function FounderFollow({ className, size = 'md', showBadge = true }: Foun
if (!user) {
toast({
title: 'Login Required',
description: 'Please log in to follow the founder',
title: t('toastsUi.loginRequired'),
description: t('toastsUi.loginToFollowFounder'),
variant: 'destructive',
});
return;
@ -75,8 +77,8 @@ export function FounderFollow({ className, size = 'md', showBadge = true }: Foun
if (isFollowing) {
toast({
title: 'Already Following',
description: `You're already following ${displayName}!`,
title: t('toastsUi.alreadyFollowing'),
description: t('toastsUi.alreadyFollowingDesc', { name: displayName }),
});
return;
}
@ -91,15 +93,15 @@ export function FounderFollow({ className, size = 'md', showBadge = true }: Foun
} catch (error) {
console.error('[FounderFollow] Failed to follow:', error);
}
}, [user, isFollowing, follow, displayName, cleanup, toast]);
}, [user, isFollowing, follow, displayName, cleanup, toast, t]);
const handleHoldStart = useCallback((e: React.TouchEvent | React.MouseEvent) => {
e.preventDefault();
if (isFollowing) {
toast({
title: 'Already Following',
description: `You're already following ${displayName}!`,
title: t('toastsUi.alreadyFollowing'),
description: t('toastsUi.alreadyFollowingDesc', { name: displayName }),
});
return;
}
@ -120,7 +122,7 @@ export function FounderFollow({ className, size = 'md', showBadge = true }: Foun
// Timeout as backup
holdTimer.current = setTimeout(handleHoldComplete, HOLD_DURATION);
}, [isFollowing, displayName, handleHoldComplete, toast]);
}, [isFollowing, displayName, handleHoldComplete, toast, t]);
const handleHoldEnd = useCallback(() => {
cleanup();

View file

@ -6,6 +6,7 @@ import { NSchema as n } from '@nostrify/nostrify';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAppContext } from '@/hooks/useAppContext';
import { useToast } from '@/hooks/useToast';
import { useTranslation } from '@/contexts/language';
// Citrine local relay (Android app by greenart7c3)
const CITRINE_URL = 'ws://localhost:4869';
@ -40,6 +41,7 @@ async function isCitrineAvailable(): Promise<boolean> {
}
export function NostrSync() {
const { t } = useTranslation();
const { nostr } = useNostr();
const { user } = useCurrentUser();
const { logins } = useNostrLogin();
@ -248,8 +250,8 @@ export function NostrSync() {
// New account — no profile published yet, nothing to sync.
} else {
toast({
title: 'Could not load profile',
description: 'Check your internet connection and try refreshing.',
title: t('toastsUi.couldNotLoadProfile'),
description: t('toastsUi.couldNotLoadProfileDesc'),
variant: 'destructive',
});
}

View file

@ -18,16 +18,17 @@ import { useRecentReports, reportTargetMeta, type RecentReport } from '@/hooks/u
import { useAdminHide, useAdminVerify, useAdminClear } from '@/hooks/useAdminHide';
import { useAdminModeration } from '@/hooks/useAdminModeration';
import { useToast } from '@/hooks/useToast';
import { useTranslation } from '@/contexts/language';
import { genUserName } from '@/lib/genUserName';
/** "3 h ago" — small enough to keep local. */
function ago(unixSeconds: number): string {
/** "3 h ago" — uses the shared `routeUi.age*` keys so it localizes. */
function ago(unixSeconds: number, t: (key: string, params?: Record<string, string | number>) => string): string {
const mins = Math.round((Date.now() / 1000 - unixSeconds) / 60);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins} min ago`;
if (mins < 1) return t('routeUi.ageJustNow');
if (mins < 60) return t('routeUi.ageMinAgo', { count: mins });
const hours = Math.round(mins / 60);
if (hours < 24) return `${hours} h ago`;
return `${Math.round(hours / 24)} d ago`;
if (hours < 24) return t('routeUi.ageHourAgo', { count: hours });
return t('routeUi.ageDayAgo', { count: Math.round(hours / 24) });
}
function Reporter({ pubkey }: { pubkey: string }) {
@ -37,6 +38,7 @@ function Reporter({ pubkey }: { pubkey: string }) {
}
function ReportRow({ report }: { report: RecentReport }) {
const { t } = useTranslation();
const { hideContent, isPending: hiding } = useAdminHide();
const { confirmContent, isPending: confirming } = useAdminVerify();
const { clearVerdict, isPending: clearing } = useAdminClear();
@ -56,8 +58,8 @@ function ReportRow({ report }: { report: RecentReport }) {
toast({ title });
} catch (e) {
toast({
title: 'Could not complete that',
description: e instanceof Error ? e.message : 'Try again.',
title: t('toastsUi.couldNotComplete'),
description: e instanceof Error ? e.message : t('toastsUi.tryAgainLower'),
variant: 'destructive',
});
}
@ -74,17 +76,17 @@ function ReportRow({ report }: { report: RecentReport }) {
{hidden && (
<Badge className="border border-red-500/40 bg-red-500/10 text-[10px] font-bold uppercase tracking-wide text-red-300">
<EyeOff className="mr-1 h-3 w-3" />
Hidden
{t('toastsUi.hiddenToast')}
</Badge>
)}
{verified && (
<Badge className="border border-emerald-500/40 bg-emerald-500/10 text-[10px] font-bold uppercase tracking-wide text-emerald-300">
<BadgeCheck className="mr-1 h-3 w-3" />
Confirmed
{t('toastsUi.confirmed')}
</Badge>
)}
<span className="text-[11px] text-gray-500">
reported by <Reporter pubkey={report.reporter} /> · {ago(report.createdAt)}
{t('miscUi.reportedBy')} <Reporter pubkey={report.reporter} /> · {ago(report.createdAt, t)}
</span>
</div>
@ -129,12 +131,12 @@ function ReportRow({ report }: { report: RecentReport }) {
id: target.id,
reason: `reported (${report.reason}) by ${report.reporter}`,
}),
'Hidden',
t('toastsUi.hiddenToast'),
)
}
>
{hiding ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <EyeOff className="h-3.5 w-3.5" />}
Hide
{t('common.hide')}
</Button>
)}
{target && !hidden && !verified && (
@ -145,12 +147,12 @@ function ReportRow({ report }: { report: RecentReport }) {
onClick={() =>
void act(
() => confirmContent({ coord, id: target.id, reason: 'report reviewed — no action' }),
'Confirmed',
t('toastsUi.confirmed'),
)
}
>
{confirming ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <BadgeCheck className="h-3.5 w-3.5" />}
Dismiss report
{t('miscUi.dismissReport')}
</Button>
)}
{target && (hidden || verified) && (
@ -162,12 +164,12 @@ function ReportRow({ report }: { report: RecentReport }) {
onClick={() =>
void act(
() => clearVerdict({ coord, id: target.id, reason: 'verdict retracted' }),
'Undone',
t('toastsUi.undone'),
)
}
>
{clearing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Undo2 className="h-3.5 w-3.5" />}
Undo
{t('common.undo')}
</Button>
)}
{target && meta?.path && target.identifier && (

View file

@ -50,6 +50,8 @@ const en = {
clear: 'Clear',
clearFilters: 'Clear Filters',
clearAll: 'Clear All',
hide: 'Hide',
undo: 'Undo',
},
// Navigation
@ -6860,6 +6862,8 @@ walletUi: {
collective: 'Collective',
collectiveDesc: 'Crew, studio or company',
membersLabel: 'Members',
dismissReport: 'Dismiss report',
reportedBy: 'reported by',
entityMembersHint: 'Add the Nostr accounts of your crew. Each member confirms their own spot with their key — until then they show as “pending”.',
entityVerifiedHint: 'Membership is verified per member — nobody can be added to your crew without confirming from their own account.',
entityInvalidPubkey: 'Enter a valid npub, nprofile, or 64-char hex pubkey.',
@ -7056,6 +7060,23 @@ walletUi: {
paymentReceivedDesc: 'Successfully received {{amount}} sats!',
paymentSent: 'Payment sent',
paymentSentDesc: 'Lightning payment completed successfully',
loginRequired: 'Login Required',
loginToFollowFounder: 'Please log in to follow the founder',
alreadyFollowing: 'Already Following',
alreadyFollowingDesc: "You're already following {{name}}!",
couldNotComplete: 'Could not complete that',
tryAgainLower: 'Try again.',
couldNotLoadProfile: 'Could not load profile',
couldNotLoadProfileDesc: 'Check your internet connection and try refreshing.',
undone: 'Undone',
undoneDesc: 'Back to neutral — not hidden, not vouched.',
confirmed: 'Confirmed',
confirmedDesc: 'Vouched for — it drops off this queue.',
hiddenToast: 'Hidden',
couldNotUndo: 'Could not undo',
couldNotConfirm: 'Could not confirm',
deleteFailed: 'Delete failed',
somethingWentWrong: 'Something went wrong',
},
cardUi: {
save: 'Save card to Nostr',

View file

@ -51,6 +51,8 @@ const es = {
clear: 'Limpiar',
clearFilters: 'Limpiar Filtros',
clearAll: 'Limpiar Todo',
hide: 'Ocultar',
undo: 'Deshacer',
},
// Navigation
@ -6860,6 +6862,8 @@ walletUi: {
collective: 'Colectivo',
collectiveDesc: 'Crew, estudio o empresa',
membersLabel: 'Miembros',
dismissReport: 'Descartar reporte',
reportedBy: 'reportado por',
entityMembersHint: 'Añade las cuentas Nostr de tu crew. Cada miembro confirma su propio spot con su clave — hasta entonces aparecen como “pendientes”.',
entityVerifiedHint: 'La membresía se verifica por miembro — nadie puede añadirse a tu crew sin confirmarlo desde su propia cuenta.',
entityInvalidPubkey: 'Introduce un npub, nprofile o clave hex de 64 caracteres válidos.',
@ -7056,6 +7060,23 @@ walletUi: {
paymentReceivedDesc: '¡Recibidos {{amount}} sats con éxito!',
paymentSent: 'Pago enviado',
paymentSentDesc: 'Pago Lightning completado correctamente',
loginRequired: 'Inicio de sesión requerido',
loginToFollowFounder: 'Inicia sesión para seguir al fundador',
alreadyFollowing: 'Ya lo sigues',
alreadyFollowingDesc: '¡Ya sigues a {{name}}!',
couldNotComplete: 'No se pudo completar eso',
tryAgainLower: 'Inténtalo de nuevo.',
couldNotLoadProfile: 'No se pudo cargar el perfil',
couldNotLoadProfileDesc: 'Revisa tu conexión a internet y prueba a actualizar.',
undone: 'Deshecho',
undoneDesc: 'Vuelto a neutro — ni oculto ni avalado.',
confirmed: 'Confirmado',
confirmedDesc: 'Avalado — sale de esta cola.',
hiddenToast: 'Oculto',
couldNotUndo: 'No se pudo deshacer',
couldNotConfirm: 'No se pudo confirmar',
deleteFailed: 'Falló la eliminación',
somethingWentWrong: 'Algo salió mal',
},
cardUi: {
save: 'Guardar carné en Nostr',

View file

@ -399,8 +399,8 @@ function QueueList({
onClick={() =>
void act(
() => clearVerdict({ coord, id: row.id, reason: 'verdict retracted' }),
{ title: 'Undone', description: 'Back to neutral — not hidden, not vouched.' },
'Could not undo',
{ title: t('toastsUi.undone'), description: t('toastsUi.undoneDesc') },
t('toastsUi.couldNotUndo'),
)
}
>
@ -416,8 +416,8 @@ function QueueList({
onClick={() =>
void act(
() => confirmContent({ coord, id: row.id, reason: 'looks legit after review' }),
{ title: 'Confirmed', description: 'Vouched for — it drops off this queue.' },
'Could not confirm',
{ title: t('toastsUi.confirmed'), description: t('toastsUi.confirmedDesc') },
t('toastsUi.couldNotConfirm'),
)
}
>