revert fb8e77bb68
Local Build and Deploy / deploy (push) Successful in 1m11s

revert style: clean up code with prettier and eslint
This commit is contained in:
remco committed 2026-07-12 21:02:03 +02:00
1 parent 27de078f54
commit e85e4d74ea
378 files changed
+20710 -22428

No files matched your search

@@ -1,88 +1,94 @@
"use client";
'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";
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<string, string>;
parseError: string | null;
files: ClientTranslationFile[]
activeFile: ClientTranslationFile
entries: Record<string, string>
parseError: string | null
}
export function ClientTranslations({ files, activeFile, entries, parseError }: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [data, setData] = useState<Record<string, string>>(entries);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const { run, isPending } = useServerAction();
const PER_PAGE = 50;
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [data, setData] = useState<Record<string, string>>(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]);
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 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);
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()}`);
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 }));
setData((d) => ({ ...d, [key]: value }))
}
function handleSave() {
if (activeFile.readOnly) return;
if (activeFile.readOnly) return
run(() => saveClientTranslations({ fileId: activeFile.id, data }), {
successMessage: "File salvato.",
successMessage: 'File salvato.',
onSuccess: (result) => {
if (result?.commentsLost) {
toast.warning(
"Alcune chiavi non corrispondevano al formato standard: file riserializzato e commenti rimossi.",
);
'Alcune chiavi non corrispondevano al formato standard: file riserializzato e commenti rimossi.',
)
}
router.refresh();
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);
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 (
@@ -96,8 +102,12 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
<Button variant="outline" size="sm" onClick={handleExport}>
<Download className="w-4 h-4 mr-1" /> Export
</Button>
<Button size="sm" onClick={handleSave} disabled={isPending || activeFile.readOnly || !!parseError}>
<Save className="w-4 h-4 mr-1" /> {isPending ? "Salvataggio..." : "Salva"}
<Button
size="sm"
onClick={handleSave}
disabled={isPending || activeFile.readOnly || !!parseError}
>
<Save className="w-4 h-4 mr-1" /> {isPending ? 'Salvataggio...' : 'Salva'}
</Button>
</div>
</div>
@@ -111,8 +121,10 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
{files.map((f) => (
<SelectItem key={f.id} value={f.id}>
<div className="flex items-center gap-2">
<span className="font-mono text-xs">{f.relPath.replace("public/", "")}</span>
{f.readOnly && <span className="text-[10px] uppercase text-muted-foreground">RO</span>}
<span className="font-mono text-xs">{f.relPath.replace('public/', '')}</span>
{f.readOnly && (
<span className="text-[10px] uppercase text-muted-foreground">RO</span>
)}
</div>
</SelectItem>
))}
@@ -123,8 +135,8 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
placeholder="Cerca per chiave o valore..."
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
setSearch(e.target.value)
setPage(1)
}}
className="max-w-xs"
/>
@@ -137,7 +149,7 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
{activeFile.format.toUpperCase()}
</Badge>
<Badge variant="outline" className="text-[11px]">
{activeFile.language === "shared" ? "shared" : activeFile.language.toUpperCase()}
{activeFile.language === 'shared' ? 'shared' : activeFile.language.toUpperCase()}
</Badge>
{activeFile.readOnly && (
<Badge variant="secondary" className="gap-1 text-[11px]">
@@ -161,8 +173,8 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
<div className="rounded-md border border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)] px-3 py-2 text-xs text-[var(--admin-warning)] dark:text-[var(--admin-warning)] flex items-start gap-2">
<Lock className="w-4 h-4 flex-shrink-0 mt-0.5" />
<div>
Questo file è di sola lettura.{" "}
{activeFile.note ?? "Modificarlo manualmente non è supportato dal CMS."}
Questo file è di sola lettura.{' '}
{activeFile.note ?? 'Modificarlo manualmente non è supportato dal CMS.'}
</div>
</div>
)}
@@ -171,9 +183,9 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
<div className="rounded-md border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-3 py-2 text-xs text-[var(--admin-info)] dark:text-[var(--admin-info)] flex items-start gap-2">
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
<div>
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.
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.
</div>
</div>
)}
@@ -190,7 +202,10 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
)}
{pageKeys.map((key) => (
<div key={key} className="flex items-center gap-3 px-4 py-2">
<span className="text-xs font-mono text-muted-foreground w-72 truncate flex-shrink-0" title={key}>
<span
className="text-xs font-mono text-muted-foreground w-72 truncate flex-shrink-0"
title={key}
>
{key}
</span>
<Input
@@ -209,7 +224,12 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
Pagina {page} di {totalPages}
</span>
<div className="flex gap-1">
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => setPage(page - 1)}>
<Button
variant="outline"
size="sm"
disabled={page <= 1}
onClick={() => setPage(page - 1)}
>
Prev
</Button>
<Button
@@ -224,5 +244,5 @@ export function ClientTranslations({ files, activeFile, entries, parseError }: P
</div>
)}
</div>
);
)
}
+21 -18
View File
@@ -1,40 +1,43 @@
import fs from "node:fs/promises";
import path from "node:path";
import JSON5 from "json5";
import { CLIENT_TRANSLATION_FILES, getClientTranslationFile } from "@/lib/client-translation-files";
import { ClientTranslations } from "./client-translations";
import fs from 'node:fs/promises'
import path from 'node:path'
import JSON5 from 'json5'
import { CLIENT_TRANSLATION_FILES, getClientTranslationFile } from '@/lib/client-translation-files'
import { ClientTranslations } from './client-translations'
export default async function ClientTranslationsPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | undefined>>;
searchParams: Promise<Record<string, string | undefined>>
}) {
const sp = await searchParams;
const requested = sp.file;
const sp = await searchParams
const requested = sp.file
const file =
(requested && getClientTranslationFile(requested)) ||
CLIENT_TRANSLATION_FILES.find((f) => !f.readOnly) ||
CLIENT_TRANSLATION_FILES[0];
CLIENT_TRANSLATION_FILES[0]
const entries: Record<string, string> = {};
let parseError: string | null = null;
const entries: Record<string, string> = {}
let parseError: string | null = null
try {
// file.relPath comes from CLIENT_TRANSLATION_FILES (closed enum) but
// Turbopack's tracer can't prove that — hint avoids tracing the whole project.
const raw = await fs.readFile(path.join(/*turbopackIgnore: true*/ process.cwd(), file.relPath), "utf-8");
const parsed = file.format === "json5" ? JSON5.parse(raw) : JSON.parse(raw);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
const raw = await fs.readFile(
path.join(/*turbopackIgnore: true*/ process.cwd(), file.relPath),
'utf-8',
)
const parsed = file.format === 'json5' ? JSON5.parse(raw) : JSON.parse(raw)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
// Nitro localization files are flat: { "key.with.dots": "value" }.
// Coerce values to strings so the editor can render them in <input>.
for (const [k, v] of Object.entries(parsed)) {
entries[k] = v == null ? "" : String(v);
entries[k] = v == null ? '' : String(v)
}
} else {
parseError = "File root is not an object.";
parseError = 'File root is not an object.'
}
} catch (err) {
parseError = err instanceof Error ? err.message : "Failed to read file.";
parseError = err instanceof Error ? err.message : 'Failed to read file.'
}
return (
@@ -44,5 +47,5 @@ export default async function ClientTranslationsPage({
entries={entries}
parseError={parseError}
/>
);
)
}