diff --git a/src/actions/translations.ts b/src/actions/translations.ts
index 9f15d916..3a32e910 100644
--- a/src/actions/translations.ts
+++ b/src/actions/translations.ts
@@ -10,7 +10,7 @@ import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
const saveTranslationsSchema = z.object({
- locale: z.enum(["en", "it"]),
+ locale: z.enum(["en", "it", "nl", "de", "fr", "es"]),
data: z.record(z.string(), z.unknown()),
});
diff --git a/src/app/admin/import/page.tsx b/src/app/admin/import/page.tsx
new file mode 100644
index 00000000..dab2153f
--- /dev/null
+++ b/src/app/admin/import/page.tsx
@@ -0,0 +1,50 @@
+import { BadgeCheck, Cat, Copy, Dumbbell, Puzzle, Shirt, Wrench } from "lucide-react";
+import Link from "next/link";
+import { redirect } from "next/navigation";
+import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
+
+const SECTIONS = [
+ { href: "/admin/import/badges", label: "Badges", icon: BadgeCheck, description: "Import and manage badge assets from external sources." },
+ { href: "/admin/import/furni", label: "Furni", icon: Puzzle, description: "Import furniture definitions, SWF files, and icons." },
+ { href: "/admin/import/clothing", label: "Clothing", icon: Shirt, description: "Import clothing and wearables for avatars." },
+ { href: "/admin/import/effects", label: "Effects", icon: Dumbbell, description: "Import avatar effects and animations." },
+ { href: "/admin/import/pets", label: "Pets", icon: Cat, description: "Import pet data and icons." },
+ { href: "/admin/import/clone", label: "Clone", icon: Copy, description: "Clone existing items from one source to another." },
+ { href: "/admin/import/repair", label: "Repair", icon: Wrench, description: "Audit and repair imported assets." },
+] as const;
+
+export default async function ImportPage() {
+ const { session, permissions } = await getAdminContext();
+ if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
+ redirect("/admin");
+ }
+
+ return (
+
+
+
Import
+
Import assets from external sources into your hotel.
+
+
+
+ {SECTIONS.map(({ href, label, icon: Icon, description }) => (
+
+
+
+
+
+
+
{label}
+
{description}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/app/admin/translations/cms/page.tsx b/src/app/admin/translations/cms/page.tsx
index 0a2f4de7..101d30fb 100644
--- a/src/app/admin/translations/cms/page.tsx
+++ b/src/app/admin/translations/cms/page.tsx
@@ -2,10 +2,17 @@ import fs from "node:fs/promises";
import path from "node:path";
import { TranslationsClient } from "../translations-client";
+const LOCALES = ["en", "it", "nl", "de", "fr", "es"] as const;
+
export default async function CmsTranslationsPage() {
const messagesDir = path.join(process.cwd(), "messages");
- const enContent = await fs.readFile(path.join(messagesDir, "en.json"), "utf-8");
- const itContent = await fs.readFile(path.join(messagesDir, "it.json"), "utf-8");
- return ;
+ const contents: Record = {};
+ await Promise.all(
+ LOCALES.map(async (locale) => {
+ contents[locale] = await fs.readFile(path.join(messagesDir, `${locale}.json`), "utf-8");
+ }),
+ );
+
+ return ;
}
diff --git a/src/app/admin/translations/translations-client.tsx b/src/app/admin/translations/translations-client.tsx
index 0a7ae74a..fe36a5c9 100644
--- a/src/app/admin/translations/translations-client.tsx
+++ b/src/app/admin/translations/translations-client.tsx
@@ -8,34 +8,49 @@ import { Input } from "@/components/ui/input";
import { useServerAction } from "@/hooks/use-server-action";
import { flattenObject, unflattenObject } from "@/lib/translations-utils";
+type Locale = "en" | "it" | "nl" | "de" | "fr" | "es";
+
+const LOCALES: { code: Locale; label: string }[] = [
+ { code: "en", label: "EN" },
+ { code: "it", label: "IT" },
+ { code: "nl", label: "NL" },
+ { code: "de", label: "DE" },
+ { code: "fr", label: "FR" },
+ { code: "es", label: "ES" },
+];
+
interface Props {
- enJson: string;
- itJson: string;
+ localeContents: Record;
}
-export function TranslationsClient({ enJson, itJson }: Props) {
- const [en, setEn] = useState(() => flattenObject(JSON.parse(enJson)));
- const [it, setIt] = useState(() => flattenObject(JSON.parse(itJson)));
+export function TranslationsClient({ localeContents }: Props) {
+ const [data, setData] = useState>>(() => {
+ const initial: Record> = {};
+ for (const [locale, content] of Object.entries(localeContents)) {
+ initial[locale] = flattenObject(JSON.parse(content));
+ }
+ return initial;
+ });
const [search, setSearch] = useState("");
const [group, setGroup] = useState("");
- const [activeLang, setActiveLang] = useState<"en" | "it">("en");
+ const [activeLang, setActiveLang] = useState("en");
const { run, isPending } = useServerAction();
const PER_PAGE = 30;
const [page, setPage] = useState(1);
- const activeData = activeLang === "en" ? en : it;
- const setActiveData = activeLang === "en" ? setEn : setIt;
+ const activeData = data[activeLang];
const groups = useMemo(() => {
const g = new Set();
- Object.keys(en).forEach((k) => {
+ Object.keys(data["en"] ?? {}).forEach((k) => {
const parts = k.split(".");
if (parts.length > 1) g.add(parts[0]);
});
return Array.from(g).sort();
- }, [en]);
+ }, [data]);
const filteredKeys = useMemo(() => {
+ if (!activeData) return [];
return Object.keys(activeData).filter((key) => {
const matchSearch =
!search ||
@@ -50,7 +65,7 @@ export function TranslationsClient({ enJson, itJson }: Props) {
const pageKeys = filteredKeys.slice((page - 1) * PER_PAGE, page * PER_PAGE);
function updateValue(key: string, value: string) {
- setActiveData({ ...activeData, [key]: value });
+ setData({ ...data, [activeLang]: { ...activeData, [key]: value } });
}
function handleSave() {
@@ -90,20 +105,19 @@ export function TranslationsClient({ enJson, itJson }: Props) {