feat(studio): localize catalog studio UI and polish responsive layout
CI / check (push) Failing after 43s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped

Route all Catalog Studio strings through the admin.studio namespace
(workspace, filters, review, inspector, quality, sync, scale32, organize
imports and page titles/subtitles) with English fallback, and add shared
StudioPage chrome plus a loading skeleton. Tighten mobile padding on the
header and sub-bars.
This commit is contained in:
openhands committed 2026-10-13 20:31:26 +02:00
1 parent d7d6382b60
commit 56676effc3
28 files changed
+1293 -634

No files matched your search

+10 -9
View File
@@ -1,5 +1,8 @@
import { ClipboardCheck } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AuditClient } from "@/app/admin/import/audit/audit-client"; import { AuditClient } from "@/app/admin/import/audit/audit-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioAuditPage(_props: { export default async function StudioAuditPage(_props: {
@@ -9,16 +12,14 @@ export default async function StudioAuditPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="space-y-6 p-4"> <StudioPage
<div> icon={ClipboardCheck}
<h1 className="text-2xl font-bold tracking-tight">Catalog Audit</h1> title={t("nav.catalogAudit")}
<p className="mt-1 text-muted-foreground"> subtitle={t("pages.auditSubtitle")}
Scan your entire furniture catalog for missing entries, broken >
references, missing files, and items available from clone sources.
</p>
</div>
<AuditClient /> <AuditClient />
</div> </StudioPage>
); );
} }
+10 -2
View File
@@ -1,5 +1,8 @@
import { BadgeCheck } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ImportBadgesClient } from "@/app/admin/import/badges/import-badges-client"; import { ImportBadgesClient } from "@/app/admin/import/badges/import-badges-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioBadgesPage(_props: { export default async function StudioBadgesPage(_props: {
@@ -9,9 +12,14 @@ export default async function StudioBadgesPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="p-4"> <StudioPage
icon={BadgeCheck}
title={t("nav.badges")}
subtitle={t("pages.badgesSubtitle")}
>
<ImportBadgesClient /> <ImportBadgesClient />
</div> </StudioPage>
); );
} }
+10 -2
View File
@@ -1,5 +1,8 @@
import { Copy } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ImportCloneClient } from "@/app/admin/import/clone/import-clone-client"; import { ImportCloneClient } from "@/app/admin/import/clone/import-clone-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioClonePage(_props: { export default async function StudioClonePage(_props: {
@@ -9,9 +12,14 @@ export default async function StudioClonePage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="p-4"> <StudioPage
icon={Copy}
title={t("nav.importFromSource")}
subtitle={t("pages.cloneSubtitle")}
>
<ImportCloneClient /> <ImportCloneClient />
</div> </StudioPage>
); );
} }
+10 -2
View File
@@ -1,5 +1,8 @@
import { Shirt } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ImportClothingClient } from "@/app/admin/import/clothing/import-clothing-client"; import { ImportClothingClient } from "@/app/admin/import/clothing/import-clothing-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioClothingPage(_props: { export default async function StudioClothingPage(_props: {
@@ -9,9 +12,14 @@ export default async function StudioClothingPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="p-4"> <StudioPage
icon={Shirt}
title={t("nav.clothing")}
subtitle={t("pages.clothingSubtitle")}
>
<ImportClothingClient /> <ImportClothingClient />
</div> </StudioPage>
); );
} }
+10 -2
View File
@@ -1,5 +1,8 @@
import { Dumbbell } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ImportEffectsClient } from "@/app/admin/import/effects/import-effects-client"; import { ImportEffectsClient } from "@/app/admin/import/effects/import-effects-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioEffectsPage(_props: { export default async function StudioEffectsPage(_props: {
@@ -9,9 +12,14 @@ export default async function StudioEffectsPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="p-4"> <StudioPage
icon={Dumbbell}
title={t("nav.effects")}
subtitle={t("pages.effectsSubtitle")}
>
<ImportEffectsClient /> <ImportEffectsClient />
</div> </StudioPage>
); );
} }
+30
View File
@@ -0,0 +1,30 @@
import { Skeleton } from "@/components/ui/skeleton";
/**
* Loading state for any Catalog Studio sub-page. Mirrors the StudioPage
* header (icon tile, title, subtitle) and a body block so navigation between
* studio tools shows a stable skeleton instead of a blank pause.
*/
export default function StudioLoading() {
return (
<div className="p-4">
<div className="space-y-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<Skeleton className="h-10 w-10 shrink-0 rounded-xl bg-[var(--admin-accent)]/10" />
<div className="min-w-0">
<Skeleton className="h-8 w-48" />
<Skeleton className="mt-2 h-4 w-72 max-w-full" />
</div>
</div>
</div>
<div className="space-y-3 rounded-xl border border-[var(--admin-border)] p-4">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-9 w-32" />
</div>
</div>
</div>
);
}
+23 -14
View File
@@ -1,7 +1,9 @@
import { Wrench } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { CatalogIntegrityPanel } from "@/components/admin/catalog/catalog-integrity-panel"; import { CatalogIntegrityPanel } from "@/components/admin/catalog/catalog-integrity-panel";
import { CatalogMaintenancePanel } from "@/components/admin/catalog/catalog-maintenance-panel"; import { CatalogMaintenancePanel } from "@/components/admin/catalog/catalog-maintenance-panel";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioCatalogMaintenancePage() { export default async function StudioCatalogMaintenancePage() {
@@ -16,20 +18,27 @@ export default async function StudioCatalogMaintenancePage() {
session.user.rank, session.user.rank,
); );
const t = await getTranslations("pages.admin.catalog.integrity"); const t = await getTranslations("pages.admin.catalog.integrity");
const studioT = await getTranslations("admin.studio");
return ( return (
<div className="min-h-0 space-y-6"> <StudioPage
<CatalogIntegrityPanel canRepair={canRepair} /> icon={Wrench}
{canRepair && ( title={studioT("nav.catalogMaintenance")}
<details className="rounded-xl border p-4"> subtitle={studioT("pages.maintenanceSubtitle")}
<summary className="cursor-pointer font-medium"> >
{t("legacyTitle")} <div className="min-h-0 space-y-6">
</summary> <CatalogIntegrityPanel canRepair={canRepair} />
<p className="mt-3 text-sm text-muted-foreground"> {canRepair && (
{t("legacyHint")} <details className="rounded-xl border p-4">
</p> <summary className="cursor-pointer font-medium">
<CatalogMaintenancePanel /> {t("legacyTitle")}
</details> </summary>
)} <p className="mt-3 text-sm text-muted-foreground">
</div> {t("legacyHint")}
</p>
<CatalogMaintenancePanel />
</details>
)}
</div>
</StudioPage>
); );
} }
+10 -9
View File
@@ -1,5 +1,8 @@
import { FileX2 } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { NitroCleanupPanel } from "@/components/admin/studio/nitro-cleanup-panel"; import { NitroCleanupPanel } from "@/components/admin/studio/nitro-cleanup-panel";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioNitroCleanupPage() { export default async function StudioNitroCleanupPage() {
@@ -7,16 +10,14 @@ export default async function StudioNitroCleanupPage() {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="space-y-6 p-4"> <StudioPage
<div> icon={FileX2}
<h1 className="text-2xl font-bold tracking-tight">Nitro cleanup</h1> title={t("nav.nitroCleanup")}
<p className="mt-1 text-muted-foreground"> subtitle={t("pages.nitroCleanupSubtitle")}
Scan the nitro directories for fake bundles (no matching furniture) >
and broken bundles (unparseable) and remove them.
</p>
</div>
<NitroCleanupPanel /> <NitroCleanupPanel />
</div> </StudioPage>
); );
} }
+10 -2
View File
@@ -1,5 +1,8 @@
import { Cat } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ImportPetsClient } from "@/app/admin/import/pets/import-pets-client"; import { ImportPetsClient } from "@/app/admin/import/pets/import-pets-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioPetsPage(_props: { export default async function StudioPetsPage(_props: {
@@ -9,9 +12,14 @@ export default async function StudioPetsPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="p-4"> <StudioPage
icon={Cat}
title={t("nav.pets")}
subtitle={t("pages.petsSubtitle")}
>
<ImportPetsClient /> <ImportPetsClient />
</div> </StudioPage>
); );
} }
+10 -12
View File
@@ -1,5 +1,8 @@
import { Image as ImageIcon } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { RepairIconsClient } from "@/app/admin/import/repair-icons/repair-icons-client"; import { RepairIconsClient } from "@/app/admin/import/repair-icons/repair-icons-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioRepairIconsPage(_props: { export default async function StudioRepairIconsPage(_props: {
@@ -9,19 +12,14 @@ export default async function StudioRepairIconsPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="space-y-6 p-4"> <StudioPage
<div> icon={ImageIcon}
<h1 className="text-2xl font-bold tracking-tight"> title={t("pages.repairIconsTitle")}
Repair Missing Icons subtitle={t("pages.repairIconsSubtitle")}
</h1> >
<p className="mt-1 text-muted-foreground">
Scan all existing furniture and download any missing icon files. Icons
are extracted from local .nitro bundles or fetched from configured
clone sources.
</p>
</div>
<RepairIconsClient /> <RepairIconsClient />
</div> </StudioPage>
); );
} }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+30 -29
View File
@@ -1,6 +1,9 @@
import { RefreshCw } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { OfficialSyncClient } from "@/app/admin/import/official/official-sync-client"; import { OfficialSyncClient } from "@/app/admin/import/official/official-sync-client";
import { SyncAllClient } from "@/app/admin/import/sync/sync-all-client"; import { SyncAllClient } from "@/app/admin/import/sync/sync-all-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { CatalogExportClient } from "./catalog-export-client"; import { CatalogExportClient } from "./catalog-export-client";
@@ -12,44 +15,42 @@ export default async function StudioSyncPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="space-y-8"> <StudioPage
<CatalogExportClient icon={RefreshCw}
canConfigure={canAccess( title={t("nav.syncRepository")}
permissions, subtitle={t("sync.pageSubtitle")}
PERMS.SETTINGS_EDIT, >
session.user.rank, <div className="space-y-6">
)} <CatalogExportClient
/> canConfigure={canAccess(
<h1 className="text-2xl font-bold tracking-tight">Sync All Sources</h1> permissions,
<p className="text-muted-foreground mt-2"> PERMS.SETTINGS_EDIT,
Scan configured sources and automatically import any furniture that is session.user.rank,
not yet present in your hotel. )}
</p> />
<div className="space-y-4"> <section className="space-y-3 rounded-lg border border-[var(--admin-border)] p-4">
<div> <h2 className="text-lg font-semibold">
<h2 className="text-lg font-semibold text-muted-foreground mb-2"> {t("sync.cloneSourcesTitle")}
Clone Sources
</h2> </h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-[var(--admin-text-muted)]">
Scan all configured clone sources and automatically import any {t("sync.cloneSourcesBody")}
furniture that is not yet present in your hotel.
</p> </p>
<SyncAllClient /> <SyncAllClient />
</div> </section>
<div> <section className="space-y-3 rounded-lg border border-[var(--admin-border)] p-4">
<h2 className="text-lg font-semibold text-muted-foreground mb-2"> <h2 className="text-lg font-semibold">
Official Furnidata {t("sync.officialFurnidataTitle")}
</h2> </h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-[var(--admin-text-muted)]">
Scan the official Habbo furnidata and automatically import any {t("sync.officialFurnidataBody")}
furniture that your hotel doesn't have yet.
</p> </p>
<OfficialSyncClient /> <OfficialSyncClient />
</div> </section>
</div> </div>
</div> </StudioPage>
); );
} }
+10 -12
View File
@@ -1,5 +1,8 @@
import { Upload } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { UploadFurniClient } from "@/app/admin/import/furni/upload/upload-furni-client"; import { UploadFurniClient } from "@/app/admin/import/furni/upload/upload-furni-client";
import { StudioPage } from "@/components/admin/studio/studio-page";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function StudioUploadPage(_props: { export default async function StudioUploadPage(_props: {
@@ -9,19 +12,14 @@ export default async function StudioUploadPage(_props: {
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) { if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin"); redirect("/admin");
} }
const t = await getTranslations("admin.studio");
return ( return (
<div className="space-y-6 p-4"> <StudioPage
<div> icon={Upload}
<h1 className="text-2xl font-bold tracking-tight"> title={t("pages.uploadTitle")}
Upload .hab Furniture subtitle={t("pages.uploadSubtitle")}
</h1> >
<p className="mt-1 text-muted-foreground">
Upload a .hab bundle and optional icon to import furniture into the
hotel. The database entry, catalog item, and FurnitureData.json will
be updated automatically.
</p>
</div>
<UploadFurniClient /> <UploadFurniClient />
</div> </StudioPage>
); );
} }
+4
View File
@@ -2,6 +2,10 @@
`studio-client.tsx` composes the workspace, selection and actions. Keep reusable data handling outside its render tree. `studio-client.tsx` composes the workspace, selection and actions. Keep reusable data handling outside its render tree.
## Shared chrome
- `studio-page.tsx`: consistent header (icon tile, title, subtitle, actions) and padding for every studio sub-page; wraps `AdminPageShell`.
## Client responsibilities ## Client responsibilities
- `studio-filters.ts`: pure filtering and sorting of the loaded furniture list; never mutates the input array. - `studio-filters.ts`: pure filtering and sorting of the loaded furniture list; never mutates the input array.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -851,10 +851,10 @@ export function OrganizeImportsDialog({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="0" className="text-xs"> <SelectItem value="0" className="text-xs">
Duckets {t("pointsTypeDuckets")}
</SelectItem> </SelectItem>
<SelectItem value="5" className="text-xs"> <SelectItem value="5" className="text-xs">
Diamonds {t("pointsTypeDiamonds")}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -1571,7 +1571,7 @@ export function OrganizeImportsDialog({
</span> </span>
</div> </div>
<Badge variant="outline" className="text-[10px]"> <Badge variant="outline" className="text-[10px]">
{previewGroup.items.length} items {previewGroup.items.length} {t("itemsUnit")}
</Badge> </Badge>
</div> </div>
{previewGroup.layout && {previewGroup.layout &&
@@ -1,6 +1,7 @@
"use client"; "use client";
import { FolderTree } from "lucide-react"; import { FolderTree } from "lucide-react";
import { useTranslations } from "next-intl";
import { lazy, Suspense, useState } from "react"; import { lazy, Suspense, useState } from "react";
import { StudioPanelLoading } from "./studio-panel-loading"; import { StudioPanelLoading } from "./studio-panel-loading";
@@ -11,6 +12,7 @@ const OrganizeImportsDialog = lazy(() =>
); );
export function OrganizeImportsLauncher() { export function OrganizeImportsLauncher() {
const t = useTranslations("pages.admin.catalog.workspace.organizeImports");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [requested, setRequested] = useState(false); const [requested, setRequested] = useState(false);
return ( return (
@@ -24,7 +26,7 @@ export function OrganizeImportsLauncher() {
className="flex min-h-10 items-center gap-2 rounded-lg px-3 text-sm font-medium text-[var(--admin-text-muted)] hover:bg-[var(--admin-accent)]/10" className="flex min-h-10 items-center gap-2 rounded-lg px-3 text-sm font-medium text-[var(--admin-text-muted)] hover:bg-[var(--admin-accent)]/10"
> >
<FolderTree size={16} /> <FolderTree size={16} />
Organize imports {t("title")}
</button> </button>
{requested && ( {requested && (
<Suspense <Suspense
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,37 @@
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
interface StudioPageProps {
icon?: LucideIcon;
title: string;
subtitle?: string;
actions?: ReactNode;
children: ReactNode;
}
/**
* Consistent chrome for Catalog Studio sub-pages: the shared admin page
* header (icon tile, title, subtitle, optional actions) with the studio's
* standard padding, so every tool page lines up the same way.
*/
export function StudioPage({
icon,
title,
subtitle,
actions,
children,
}: StudioPageProps) {
return (
<div className="p-3 sm:p-4">
<AdminPageShell
icon={icon}
title={title}
subtitle={subtitle}
actions={actions}
>
{children}
</AdminPageShell>
</div>
);
}
@@ -1,4 +1,5 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { setFurnidataTranslateEnabled } from "@/actions/import-furni"; import { setFurnidataTranslateEnabled } from "@/actions/import-furni";
@@ -24,6 +25,7 @@ const TRANSLATE_LANGS = [
] as const; ] as const;
export function useStudioTranslations(defaultTranslate: boolean) { export function useStudioTranslations(defaultTranslate: boolean) {
const t = useTranslations("admin.studio");
// Translation controls (per-import toggle + language selection, plus a global // Translation controls (per-import toggle + language selection, plus a global
// default that persists to the site setting and seeds the per-import toggle). // default that persists to the site setting and seeds the per-import toggle).
const ALL_LANGS = TRANSLATE_LANGS as readonly string[]; const ALL_LANGS = TRANSLATE_LANGS as readonly string[];
@@ -52,13 +54,16 @@ export function useStudioTranslations(defaultTranslate: boolean) {
const langs = data.languages ?? []; const langs = data.languages ?? [];
const okCount = langs.filter((l) => l.ok).length; const okCount = langs.filter((l) => l.ok).length;
toast.success( toast.success(
`Languages translated (${okCount}/${langs.length} completed)`, t("toasts.languagesTranslated", {
ok: okCount,
total: langs.length,
}),
); );
} else { } else {
toast.error(data.error ?? "Translation failed"); toast.error(data.error ?? t("toasts.translationFailed"));
} }
} catch { } catch {
toast.error("Network error while translating languages"); toast.error(t("toasts.networkTranslating"));
} finally { } finally {
setBuildingLanguages(false); setBuildingLanguages(false);
} }
@@ -75,7 +80,7 @@ export function useStudioTranslations(defaultTranslate: boolean) {
try { try {
await setFurnidataTranslateEnabled({ enabled: next }); await setFurnidataTranslateEnabled({ enabled: next });
} catch { } catch {
toast.error("Could not save the default translation setting"); toast.error(t("toasts.saveDefaultFailed"));
} }
}; };
File diff suppressed because it is too large. Load diff