From 94a6628faaf5817b13b1c41da65e455b21f30af7 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Wed, 7 Oct 2026 17:14:18 +0700 Subject: [PATCH] i18n(es): translate page/component toasts (follow, delete, reports, sync, review queue) --- src/components/DeleteEntityDialog.tsx | 6 +++-- src/components/FounderFollow.tsx | 18 ++++++++------ src/components/NostrSync.tsx | 6 +++-- src/components/ReportsInbox.tsx | 36 ++++++++++++++------------- src/i18n/en.ts | 21 ++++++++++++++++ src/i18n/es.ts | 21 ++++++++++++++++ src/pages/AdminReviewPage.tsx | 8 +++--- 7 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/components/DeleteEntityDialog.tsx b/src/components/DeleteEntityDialog.tsx index 31eac6f..4268ab9 100644 --- a/src/components/DeleteEntityDialog.tsx +++ b/src/components/DeleteEntityDialog.tsx @@ -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', }); } diff --git a/src/components/FounderFollow.tsx b/src/components/FounderFollow.tsx index cdd28b8..de18c06 100644 --- a/src/components/FounderFollow.tsx +++ b/src/components/FounderFollow.tsx @@ -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(); diff --git a/src/components/NostrSync.tsx b/src/components/NostrSync.tsx index c46876e..5338503 100644 --- a/src/components/NostrSync.tsx +++ b/src/components/NostrSync.tsx @@ -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 { } 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', }); } diff --git a/src/components/ReportsInbox.tsx b/src/components/ReportsInbox.tsx index eca2309..f1cfdaf 100644 --- a/src/components/ReportsInbox.tsx +++ b/src/components/ReportsInbox.tsx @@ -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 { 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 && ( - Hidden + {t('toastsUi.hiddenToast')} )} {verified && ( - Confirmed + {t('toastsUi.confirmed')} )} - reported by · {ago(report.createdAt)} + {t('miscUi.reportedBy')} · {ago(report.createdAt, t)} @@ -129,12 +131,12 @@ function ReportRow({ report }: { report: RecentReport }) { id: target.id, reason: `reported (${report.reason}) by ${report.reporter}`, }), - 'Hidden', + t('toastsUi.hiddenToast'), ) } > {hiding ? : } - Hide + {t('common.hide')} )} {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 ? : } - Dismiss report + {t('miscUi.dismissReport')} )} {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 ? : } - Undo + {t('common.undo')} )} {target && meta?.path && target.identifier && ( diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 4e2afff..ef9693d 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -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', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 85f645b..28ff639 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -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', diff --git a/src/pages/AdminReviewPage.tsx b/src/pages/AdminReviewPage.tsx index 5678c26..e9abe5b 100644 --- a/src/pages/AdminReviewPage.tsx +++ b/src/pages/AdminReviewPage.tsx @@ -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'), ) } >