"use client"; import { Download, 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 saved.", onSuccess: (result) => { if (result?.commentsLost) { toast.warning( "Some keys did not match the standard format: file was reserialized and comments were removed.", ); } 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 (
{ setSearch(e.target.value); setPage(1); }} className="max-w-xs" /> {filteredKeys.length} keys
{activeFile.format.toUpperCase()} {activeFile.language === "shared" ? "shared" : activeFile.language.toUpperCase()} {activeFile.readOnly && ( read-only )} {activeFile.hasComments && !activeFile.readOnly && ( comments preserved )}
{activeFile.note && (
{activeFile.note}
)} {activeFile.readOnly && (
This file is read-only.{" "} {activeFile.note ?? "Manual editing is not supported by the CMS."}
)} {activeFile.hasComments && !activeFile.readOnly && (
Comments are preserved: saving only patches changed value lines. If a key uses a non-standard format and cannot be patched in-place, the file is reserialized (comments lost) and you will be warned.
)} {parseError && (
Parse error: {parseError}
)}
{pageKeys.length === 0 && !parseError && (
No entries.
)} {pageKeys.map((key) => (
{key} updateValue(key, e.target.value)} className="flex-1 h-8 text-sm" disabled={activeFile.readOnly} />
))}
{totalPages > 1 && (
Page {page} of {totalPages}
)}
); }