diff --git a/src/actions/prefixes.ts b/src/actions/prefixes.ts index 93ff79a6..b0b6ddfd 100644 --- a/src/actions/prefixes.ts +++ b/src/actions/prefixes.ts @@ -116,10 +116,14 @@ const SETTINGS_WHITELIST = new Set([ "enabled", "max_length", "min_rank", + "min_rank_to_buy", "allow_colors", "allow_bold", "allow_italic", "default_color", + "price_credits", + "price_points", + "points_type", ]); const updatePrefixSettingsSchema = z.object({ diff --git a/src/app/admin/import/furni/nitro-editor-dialog.tsx b/src/app/admin/import/furni/nitro-editor-dialog.tsx index e571ff0a..a54f6c14 100644 --- a/src/app/admin/import/furni/nitro-editor-dialog.tsx +++ b/src/app/admin/import/furni/nitro-editor-dialog.tsx @@ -408,7 +408,7 @@ export function NitroEditorDialog({ htmlFor="dim-y" className="text-xs text-muted-foreground" > - Y (profondità) + Y (depth) onOpenChange(false)} disabled={saving} > - Annulla + Cancel diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 0c806e3d..05727df2 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -109,24 +109,24 @@ function getNavGroups(t: (key: string) => string) { }, { href: "/admin/events", - label: "Events", + label: t("events"), icon: , }, { href: "/admin/polls", - label: "Polls", + label: t("polls"), icon: , }, - { href: "/admin/banners", label: "Banners", icon: }, + { href: "/admin/banners", label: t("banners"), icon: }, { href: "/admin/prefixes", - label: "Prefixes", + label: t("prefixes"), icon: , }, ], }, { - label: t("usersAccess"), + label: t("usersAndAccess"), icon: , items: [ { href: "/admin/users", label: t("users"), icon: }, @@ -150,12 +150,12 @@ function getNavGroups(t: (key: string) => string) { }, { href: "/admin/online", - label: "Online Users", + label: t("onlineUsers"), icon: , }, { href: "/admin/moderation", - label: "Moderation", + label: t("moderation"), icon: , }, ], @@ -286,15 +286,15 @@ function getNavGroups(t: (key: string) => string) { { href: "/admin/logs", label: t("logs"), icon: }, { href: "/admin/logs/audit", - label: "Audit Log", + label: t("auditLog"), icon: , }, { href: "/admin/analytics", - label: "Analytics", + label: t("analytics"), icon: , }, - { href: "/admin/devops", label: "DevOps", icon: }, + { href: "/admin/devops", label: t("devops"), icon: }, { href: "/admin/wordfilter", label: t("wordFilter"), diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c2689b58..4123eb8f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -168,7 +168,7 @@ export default async function AdminDashboard() { {t("recentStaffActivity")} {t("viewAll")} diff --git a/src/app/admin/permissions/[id]/page.tsx b/src/app/admin/permissions/[id]/page.tsx index ea3be42d..954b9551 100644 --- a/src/app/admin/permissions/[id]/page.tsx +++ b/src/app/admin/permissions/[id]/page.tsx @@ -1,6 +1,6 @@ import { notFound, redirect } from "next/navigation"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; -import { getAdminContext } from "@/lib/permissions"; +import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { fetchEmulatorRankForEdit } from "@/lib/services/permission-ranks"; import { RankEditClient } from "./rank-edit-client"; @@ -11,8 +11,12 @@ export default async function RankEditPage({ params: Promise<{ locale: string; id: string }>; }) { const { id: rawId } = await params; - const { permissions } = await getAdminContext(); - if (!permissions.isSuperAdmin) redirect("/admin"); + const { session, permissions } = await getAdminContext(); + if ( + !canAccess(permissions, PERMS.PERMISSIONS_MANAGE, session.user.rank) + ) { + redirect("/admin"); + } const rankId = parseInt(rawId, 10); if (Number.isNaN(rankId)) notFound(); diff --git a/src/app/admin/permissions/page.tsx b/src/app/admin/permissions/page.tsx index f2efddca..3ab1da5f 100644 --- a/src/app/admin/permissions/page.tsx +++ b/src/app/admin/permissions/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { Prisma } from "@/generated/prisma/client"; -import { getAdminContext } from "@/lib/permissions"; +import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { fetchEmulatorRankSummaries } from "@/lib/services/permission-ranks"; import { PermissionsList } from "./permissions-list"; @@ -8,8 +8,12 @@ import { PermissionsList } from "./permissions-list"; export default async function PermissionsPage(_props: { params: Promise<{ locale: string }>; }) { - const { permissions } = await getAdminContext(); - if (!permissions.isSuperAdmin) redirect("/admin"); + const { session, permissions } = await getAdminContext(); + if ( + !canAccess(permissions, PERMS.PERMISSIONS_MANAGE, session.user.rank) + ) { + redirect("/admin"); + } const ranks = await fetchEmulatorRankSummaries(prisma); diff --git a/src/app/admin/prefixes/page.tsx b/src/app/admin/prefixes/page.tsx index 104d0697..e4d2a3ef 100644 --- a/src/app/admin/prefixes/page.tsx +++ b/src/app/admin/prefixes/page.tsx @@ -20,7 +20,7 @@ export default async function PrefixesPage() {

Custom Prefixes

- Gestisci i prefissi degli utenti, la blacklist e le impostazioni + Manage user prefixes, blacklist and settings

diff --git a/src/app/admin/prefixes/prefixes-client.tsx b/src/app/admin/prefixes/prefixes-client.tsx index 886a614e..f8143e75 100644 --- a/src/app/admin/prefixes/prefixes-client.tsx +++ b/src/app/admin/prefixes/prefixes-client.tsx @@ -60,46 +60,46 @@ interface PrefixFormData { } const PRESET_EFFECTS = [ - { id: "", label: "Nessuno" }, - { id: "glow", label: "✨ Bagliore" }, - { id: "shadow", label: "🌑 Ombra" }, - { id: "italic", label: "𝑰 Corsivo" }, - { id: "outline", label: "🔲 Contorno" }, - { id: "pulse", label: "💫 Pulsante" }, + { id: "", label: "None" }, + { id: "glow", label: "✨ Glow" }, + { id: "shadow", label: "🌑 Shadow" }, + { id: "italic", label: "𝑰 Italic" }, + { id: "outline", label: "🔲 Outline" }, + { id: "pulse", label: "💫 Pulse" }, { id: "bold-glow", label: "💡 Neon" }, ]; const PRESET_ICONS = [ - { emoji: "", label: "Nessuna" }, - { emoji: "👑", label: "Corona" }, - { emoji: "⭐", label: "Stella" }, - { emoji: "❤️", label: "Cuore" }, - { emoji: "💎", label: "Diamante" }, - { emoji: "🔥", label: "Fuoco" }, - { emoji: "⚡", label: "Fulmine" }, - { emoji: "🛡️", label: "Scudo" }, - { emoji: "💀", label: "Teschio" }, - { emoji: "🎵", label: "Musica" }, - { emoji: "💠", label: "Gemma" }, - { emoji: "⚔️", label: "Spade" }, - { emoji: "🏆", label: "Trofeo" }, - { emoji: "🚀", label: "Razzo" }, - { emoji: "👻", label: "Fantasma" }, - { emoji: "🐾", label: "Zampa" }, - { emoji: "🍃", label: "Foglia" }, + { emoji: "", label: "None" }, + { emoji: "👑", label: "Crown" }, + { emoji: "⭐", label: "Star" }, + { emoji: "❤️", label: "Heart" }, + { emoji: "💎", label: "Diamond" }, + { emoji: "🔥", label: "Fire" }, + { emoji: "⚡", label: "Lightning" }, + { emoji: "🛡️", label: "Shield" }, + { emoji: "💀", label: "Skull" }, + { emoji: "🎵", label: "Music" }, + { emoji: "💠", label: "Gem" }, + { emoji: "⚔️", label: "Swords" }, + { emoji: "🏆", label: "Trophy" }, + { emoji: "🚀", label: "Rocket" }, + { emoji: "👻", label: "Ghost" }, + { emoji: "🐾", label: "Paw" }, + { emoji: "🍃", label: "Leaf" }, { emoji: "😎", label: "Cool" }, - { emoji: "🐺", label: "Lupo" }, - { emoji: "🦁", label: "Leone" }, - { emoji: "🐉", label: "Drago" }, - { emoji: "🦄", label: "Unicorno" }, - { emoji: "🌙", label: "Luna" }, - { emoji: "☀️", label: "Sole" }, - { emoji: "❄️", label: "Neve" }, - { emoji: "🌹", label: "Rosa" }, + { emoji: "🐺", label: "Wolf" }, + { emoji: "🦁", label: "Lion" }, + { emoji: "🐉", label: "Dragon" }, + { emoji: "🦄", label: "Unicorn" }, + { emoji: "🌙", label: "Moon" }, + { emoji: "☀️", label: "Sun" }, + { emoji: "❄️", label: "Snow" }, + { emoji: "🌹", label: "Rose" }, { emoji: "🎮", label: "Controller" }, - { emoji: "💰", label: "Soldi" }, - { emoji: "🔮", label: "Sfera" }, - { emoji: "🏴‍☠️", label: "Pirata" }, + { emoji: "💰", label: "Money" }, + { emoji: "🔮", label: "Orb" }, + { emoji: "🏴‍☠️", label: "Pirate" }, ]; function PrefixDialog({ @@ -167,7 +167,7 @@ function PrefixDialog({

- {editPrefix ? "Modifica Prefisso" : "Aggiungi Prefisso"} + {editPrefix ? "Edit Prefix" : "Add Prefix"}

@@ -338,6 +338,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { const params = new URLSearchParams({ page: String(page) }); if (searchQuery) params.set("q", searchQuery); const res = await fetch(`/api/admin/prefixes?${params}`); + if (!res.ok) return; const data = await res.json(); if (data.ok) { setPrefixes(data.prefixes); @@ -347,12 +348,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { const fetchBlacklist = useCallback(async () => { const res = await fetch("/api/admin/prefixes/blacklist"); + if (!res.ok) return; const data = await res.json(); if (data.ok) setBlacklist(data.words); }, []); const fetchSettings = useCallback(async () => { const res = await fetch("/api/admin/prefixes/settings"); + if (!res.ok) return; const data = await res.json(); if (data.ok) setSettings(data.settings); }, []); @@ -367,9 +370,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { }, [fetchBlacklist, fetchSettings]); const handleDeletePrefix = async (id: number) => { - if (!confirm("Sei sicuro di voler eliminare questo prefisso?")) return; + if (!confirm("Are you sure you want to delete this prefix?")) return; run(() => deletePrefixAction({ id }), { - successMessage: "Prefisso eliminato!", + successMessage: "Prefix deleted!", onSuccess: () => { fetchPrefixes(); }, @@ -389,7 +392,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { active: data.active ? 1 : 0, }), { - successMessage: "Prefisso aggiornato!", + successMessage: "Prefix updated!", onSuccess: () => { fetchPrefixes(); }, @@ -407,7 +410,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { active: data.active ? 1 : 0, }), { - successMessage: "Prefisso creato!", + successMessage: "Prefix created!", onSuccess: () => { fetchPrefixes(); }, @@ -429,7 +432,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { const handleAddBlacklistWord = async () => { if (!newWord.trim()) return; run(() => addBlacklistWordAction({ word: newWord.trim() }), { - successMessage: "Parola aggiunta alla blacklist!", + successMessage: "Word added to blacklist!", onSuccess: () => { setNewWord(""); fetchBlacklist(); @@ -439,7 +442,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { const handleRemoveBlacklistWord = async (id: number) => { run(() => removeBlacklistWordAction({ id }), { - successMessage: "Parola rimossa dalla blacklist!", + successMessage: "Word removed from blacklist!", onSuccess: () => { fetchBlacklist(); }, @@ -453,7 +456,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { settings: settings as unknown as Record, }), { - successMessage: "Impostazioni salvate!", + successMessage: "Settings saved!", }, ); }; @@ -469,9 +472,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { - Prefissi Utenti + User Prefixes Blacklist - Impostazioni + Settings @@ -480,7 +483,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { { setSearchQuery(e.target.value); @@ -489,12 +492,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { /> {canEdit && ( )} @@ -503,14 +506,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { ID - Utente + User Prefix - Colore - Icona - Effetto - Attivo - Data - {canEdit && Azioni} + Color + Icon + Effect + Active + Date + {canEdit && Actions} @@ -580,9 +583,11 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { ?.label || prefix.effect : "-"} - {prefix.active ? "Si" : "No"} + {prefix.active ? "Yes" : "No"} - {new Date(prefix.created_at).toLocaleDateString()} + {prefix.created_at + ? new Date(prefix.created_at).toLocaleDateString() + : "—"} {canEdit && ( @@ -614,7 +619,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { className="text-center text-muted-foreground" colSpan={9} > - Nessun prefisso trovato + No prefixes found )} @@ -629,10 +634,10 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { variant="outline" onClick={() => setPage(page - 1)} > - Precedente + Previous - Pagina {page} di {totalPages} + Page {page} of {totalPages} )} @@ -649,13 +654,13 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { - Parole Vietate + Forbidden Words {canEdit && (
setNewWord(e.target.value)} onKeyDown={(e) => @@ -664,7 +669,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { />
)} @@ -689,7 +694,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { ))} {blacklist.length === 0 && (

- Nessuna parola in blacklist + No blacklist words

)} @@ -700,12 +705,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) { - Impostazioni Prefissi + Prefix Settings
- Lunghezza Massima + Max Length
- Rank Minimo + Minimum Rank
- Prezzo (Crediti) + Price (Credits)
- Prezzo (Punti) + Price (Points)
- Tipo Punti + Points Type

- 0 = Duckets, 5 = Diamonds, ecc. + 0 = Duckets, 5 = Diamonds, etc.

{canEdit && ( )}
diff --git a/src/app/admin/sounds/page.tsx b/src/app/admin/sounds/page.tsx index 6fb14ff2..643853fe 100644 --- a/src/app/admin/sounds/page.tsx +++ b/src/app/admin/sounds/page.tsx @@ -70,11 +70,11 @@ export default async function SoundsPage() {

Song Disks

- Carica e gestisci i file MP3 dei song disk riprodotti dai jukebox - in-game. I file sono serviti da {baseUrl} (configurabile - in{" "} - - Impostazioni > CMS + Upload and manage MP3 files for song disks played by in-game + jukeboxes. Files are served from {baseUrl}{" "} + (configurable in{" "} + + Settings ).

diff --git a/src/app/admin/sounds/sounds-client.tsx b/src/app/admin/sounds/sounds-client.tsx index c1ff421b..c259d408 100644 --- a/src/app/admin/sounds/sounds-client.tsx +++ b/src/app/admin/sounds/sounds-client.tsx @@ -121,8 +121,8 @@ export function SoundsClient({ if (!deleting) return; const id = deleting.id; run(() => deleteSoundtrack({ id }), { - successMessage: `Song disk #${id} eliminato.`, - errorMessage: `Impossibile eliminare il song disk #${id}.`, + successMessage: `Song disk #${id} deleted.`, + errorMessage: `Unable to delete song disk #${id}.`, onSuccess: () => { invalidateSongPickerCache(); setDeleting(null); @@ -139,7 +139,7 @@ export function SoundsClient({ setQuery(e.target.value)} - placeholder="Cerca per nome, autore, codice o id…" + placeholder="Search by name, author, code or id…" className="pl-8" />
@@ -151,9 +151,9 @@ export function SoundsClient({ - Tutti ({soundtracks.length}) + All ({soundtracks.length}) - Singoli MP3 ({counts.single}) + Single MP3s ({counts.single}) Composition ({counts.composition}) @@ -199,15 +199,15 @@ export function SoundsClient({ {soundtracks.length === 0 ? (
-

Nessun song disk caricato.

+

No song disks uploaded yet.

{canEdit && (

- Clicca “Upload MP3” per iniziare. + Click “Upload MP3” to get started.

)}
) : ( - <>Nessun risultato per i filtri attivi. + <>No results for the active filters. )} @@ -293,7 +293,7 @@ export function SoundsClient({ className="h-8 w-8" onClick={() => setEditing(s)} disabled={isPending} - title="Modifica" + title="Edit" > @@ -303,7 +303,7 @@ export function SoundsClient({ className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => setDeleting(s)} disabled={isPending} - title="Elimina" + title="Delete" > @@ -345,27 +345,26 @@ export function SoundsClient({ - Eliminare “{deleting.name}”? + Delete “{deleting.name}”? {deleting.usedBy > 0 ? ( <> - Questo song disk è attualmente usato da{" "} + This song disk is currently used by{" "} {deleting.usedBy} catalog item - {deleting.usedBy === 1 ? "" : "s"}. Eliminandolo, quei - catalog item verranno riportati a “nessun song - disk”. + {deleting.usedBy === 1 ? "" : "s"}. Deleting it will reset + those catalog items to “no song disk”. {deleting.kind === "single" && ( - <> Il file MP3 sarà rimosso dal disco. + <> The MP3 file will be removed from disk. )}{" "} - L'azione è irreversibile. + This action cannot be undone. ) : ( <> {deleting.kind === "single" - ? "Il file MP3 sarà rimosso dal disco e la riga dal database." - : "La riga sarà rimossa dal database. Nessun file viene eliminato (una composition non ha MP3 dedicato)."}{" "} - L'azione è irreversibile. + ? "The MP3 file will be removed from disk and the row from the database." + : "The row will be removed from the database. No file is deleted (a composition has no dedicated MP3)."}{" "} + This action cannot be undone. )} @@ -376,7 +375,7 @@ export function SoundsClient({ onClick={() => setDeleting(null)} disabled={isPending} > - Annulla + Cancel @@ -420,11 +419,11 @@ function UploadDialog({ e.preventDefault(); const file = fileRef.current?.files?.[0]; if (!file) { - toast.error("Seleziona un file MP3."); + toast.error("Select an MP3 file."); return; } if (!name.trim()) { - toast.error("Il nome è obbligatorio."); + toast.error("Name is required."); return; } @@ -445,16 +444,16 @@ function UploadDialog({ id?: number; }; if (!res.ok || !json.ok) { - toast.error(json.error || "Upload fallito."); + toast.error(json.error || "Upload failed."); return; } - toast.success(`Song disk #${json.id} caricato.`); + toast.success(`Song disk #${json.id} uploaded.`); reset(); onOpenChange(false); onUploaded(); } catch (err) { console.error(err); - toast.error("Errore di rete durante l'upload."); + toast.error("Network error during upload."); } finally { setUploading(false); } @@ -471,14 +470,14 @@ function UploadDialog({ Upload Song Disk - Carica un file MP3. Dimensione massima 10 MB. Il codice sarà - generato automaticamente come song_{id}. + Upload an MP3 file. Max size 10 MB. The code will be generated + automatically as song_{id}.
- +
- + setName(e.target.value)} - placeholder="Titolo del brano" + placeholder="Track title" maxLength={100} required />
- + setAuthor(e.target.value)} - placeholder="Autore (opzionale)" + placeholder="Author (optional)" maxLength={50} />
@@ -516,7 +515,7 @@ function UploadDialog({ onClick={() => onOpenChange(false)} disabled={uploading} > - Annulla + Cancel
@@ -555,12 +554,12 @@ function EditDialog({ function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!name.trim()) { - toast.error("Il nome è obbligatorio."); + toast.error("Name is required."); return; } const lenNum = Number.parseInt(length, 10); if (!Number.isFinite(lenNum) || lenNum < 0) { - toast.error("La durata deve essere un numero ≥ 0."); + toast.error("Length must be a number ≥ 0."); return; } run( @@ -573,8 +572,8 @@ function EditDialog({ length: lenNum, }), { - successMessage: "Song disk aggiornato.", - errorMessage: "Impossibile aggiornare il song disk.", + successMessage: "Song disk updated.", + errorMessage: "Unable to update the song disk.", onSuccess: onSaved, }, ); @@ -585,21 +584,21 @@ function EditDialog({ - Modifica {isComposition ? "Trax Composition" : "Song Disk"} + Edit {isComposition ? "Trax Composition" : "Song Disk"} {soundtrack.code} {isComposition ? ( <> {" "} - — composition di {soundtrack.samples.length} sample. Modifica il - campo track solo se sai cosa stai facendo. + — composition of {soundtrack.samples.length} samples. Edit the{" "} + track field only if you know what you are doing. ) : ( <> {" "} - — il file MP3 non può essere sostituito: elimina e ricarica se - devi cambiare audio. + — the MP3 file cannot be replaced: delete and re-upload if you + need to change the audio. )} @@ -607,7 +606,7 @@ function EditDialog({
- +
- +
- +

{isComposition - ? "Durata totale della composition. Lascia 0 se sconosciuta." - : "Impostato automaticamente dall’upload. Modifica solo se errato."} + ? "Total composition length. Leave 0 if unknown." + : "Set automatically from the upload. Edit only if incorrect."}

@@ -653,8 +652,8 @@ function EditDialog({ placeholder="1:371,8;374,4;378,4;0,4;…" />

- Formato: canale:sample_id,durata;sample_id,durata;…{" "} - — 0 = silenzio. Sample IDs rilevati:{" "} + Format: channel:sample_id,length;sample_id,length;…{" "} + — 0 = silence. Detected sample IDs:{" "} {soundtrack.samples.join(", ") || "—"} @@ -670,11 +669,11 @@ function EditDialog({ onClick={() => onOpenChange(false)} disabled={isPending} > - Annulla + Cancel

diff --git a/src/app/admin/translations/client/client-translations.tsx b/src/app/admin/translations/client/client-translations.tsx index ac30bd62..9c1fe597 100644 --- a/src/app/admin/translations/client/client-translations.tsx +++ b/src/app/admin/translations/client/client-translations.tsx @@ -74,11 +74,11 @@ export function ClientTranslations({ function handleSave() { if (activeFile.readOnly) return; run(() => saveClientTranslations({ fileId: activeFile.id, data }), { - successMessage: "File salvato.", + successMessage: "File saved.", onSuccess: (result) => { if (result?.commentsLost) { toast.warning( - "Alcune chiavi non corrispondevano al formato standard: file riserializzato e commenti rimossi.", + "Some keys did not match the standard format: file was reserialized and comments were removed.", ); } router.refresh(); @@ -115,7 +115,7 @@ export function ClientTranslations({ disabled={isPending || activeFile.readOnly || !!parseError} > {" "} - {isPending ? "Salvataggio..." : "Salva"} + {isPending ? "Saving..." : "Save"} @@ -144,7 +144,7 @@ export function ClientTranslations({ { setSearch(e.target.value); @@ -154,7 +154,7 @@ export function ClientTranslations({ /> - {filteredKeys.length} chiavi + {filteredKeys.length} keys @@ -174,7 +174,7 @@ export function ClientTranslations({ )} {activeFile.hasComments && !activeFile.readOnly && ( - commenti preservati + comments preserved )} @@ -189,9 +189,9 @@ export function ClientTranslations({
- Questo file è di sola lettura.{" "} + This file is read-only.{" "} {activeFile.note ?? - "Modificarlo manualmente non è supportato dal CMS."} + "Manual editing is not supported by the CMS."}
)} @@ -200,24 +200,23 @@ export function ClientTranslations({
- 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. + 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 && (
- Errore di parsing: {parseError} + Parse error: {parseError}
)}
{pageKeys.length === 0 && !parseError && (
- Nessuna voce. + No entries.
)} {pageKeys.map((key) => ( @@ -241,7 +240,7 @@ export function ClientTranslations({ {totalPages > 1 && (
- Pagina {page} di {totalPages} + Page {page} of {totalPages}