"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}
)}
); }