'use client' import { Download, Gamepad2, Info, Lock, Save } from 'lucide-react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' import { saveClientTranslations } from '@/actions/translations' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { useServerAction } from '@/hooks/use-server-action' import type { ClientTranslationFile } from '@/lib/client-translation-files' interface Props { files: ClientTranslationFile[] activeFile: ClientTranslationFile entries: Record parseError: string | null } export function ClientTranslations({ files, activeFile, entries, parseError }: Props) { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const [data, setData] = useState>(entries) const [search, setSearch] = useState('') const [page, setPage] = useState(1) const { run, isPending } = useServerAction() const PER_PAGE = 50 // Re-sync local state when the user switches file (server re-renders with // fresh entries; useState only initializes once). // biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code useEffect(() => { setData(entries) setSearch('') setPage(1) }, [entries, activeFile.id]) const filteredKeys = useMemo(() => { const keys = Object.keys(data) if (!search) return keys const q = search.toLowerCase() return keys.filter((k) => k.toLowerCase().includes(q) || data[k].toLowerCase().includes(q)) }, [data, search]) const totalPages = Math.max(1, Math.ceil(filteredKeys.length / PER_PAGE)) const pageKeys = filteredKeys.slice((page - 1) * PER_PAGE, page * PER_PAGE) function changeFile(id: string) { const params = new URLSearchParams(searchParams?.toString()) params.set('file', id) router.push(`${pathname}?${params.toString()}`) } function updateValue(key: string, value: string) { setData((d) => ({ ...d, [key]: value })) } function handleSave() { if (activeFile.readOnly) return run(() => saveClientTranslations({ fileId: activeFile.id, data }), { successMessage: 'File salvato.', onSuccess: (result) => { if (result?.commentsLost) { toast.warning( 'Alcune chiavi non corrispondevano al formato standard: file riserializzato e commenti rimossi.', ) } router.refresh() }, }) } function handleExport() { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json', }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = activeFile.relPath.split('/').pop() ?? 'translations.json' a.click() URL.revokeObjectURL(url) } return (

Client (Nitro)

{ setSearch(e.target.value) setPage(1) }} className="max-w-xs" /> {filteredKeys.length} chiavi
{activeFile.format.toUpperCase()} {activeFile.language === 'shared' ? 'shared' : activeFile.language.toUpperCase()} {activeFile.readOnly && ( read-only )} {activeFile.hasComments && !activeFile.readOnly && ( commenti preservati )}
{activeFile.note && (
{activeFile.note}
)} {activeFile.readOnly && (
Questo file è di sola lettura.{' '} {activeFile.note ?? 'Modificarlo manualmente non è supportato dal CMS.'}
)} {activeFile.hasComments && !activeFile.readOnly && (
I commenti vengono preservati: il salvataggio modifica solo le righe dei valori cambiati. Se una chiave usa un formato non standard ed è impossibile patchare in-place, il file viene riserializzato (commenti persi) e ti avviseremo.
)} {parseError && (
Errore di parsing: {parseError}
)}
{pageKeys.length === 0 && !parseError && (
Nessuna voce.
)} {pageKeys.map((key) => (
{key} updateValue(key, e.target.value)} className="flex-1 h-8 text-sm" disabled={activeFile.readOnly} />
))}
{totalPages > 1 && (
Pagina {page} di {totalPages}
)}
) }