feat: per-import translation toggle and language picker in studio
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 53s

Furni imports can now choose whether to translate (per import) and which
languages to build, instead of always rebuilding all 13 FurnitureData_<lang>
files. The studio header also has a global switch that persists the
furnidata_translate_enabled setting, seeding the per-import default.
This commit is contained in:
openhands committed 2026-08-24 18:16:42 +02:00
1 parent d6af754211
commit 6dced0fb54
5 files changed
+223 -10

No files matched your search

+18
View File
@@ -1,6 +1,7 @@
"use server"; "use server";
import { z } from "zod"; import { z } from "zod";
import { db, WebsiteSetting } from "@/lib/db";
import { PERMS } from "@/lib/permissions"; import { PERMS } from "@/lib/permissions";
import { adminAction } from "@/lib/safe-action"; import { adminAction } from "@/lib/safe-action";
import { actionOk } from "@/lib/safe-action-shared"; import { actionOk } from "@/lib/safe-action-shared";
@@ -8,6 +9,7 @@ import {
cleanupConvertedSwfs, cleanupConvertedSwfs,
deleteImportedItem, deleteImportedItem,
} from "@/lib/services/furni-import"; } from "@/lib/services/furni-import";
import { siteSettings } from "@/lib/services/site-settings";
export const cleanSwfFiles = adminAction( export const cleanSwfFiles = adminAction(
{ permission: PERMS.ASSETS_IMPORT }, { permission: PERMS.ASSETS_IMPORT },
@@ -29,3 +31,19 @@ export const deleteImportedFurni = adminAction(
>, >,
), ),
); );
const translateToggleSchema = z.object({ enabled: z.boolean() });
/** Persist the global "translate furniture names" setting from the studio. */
export const setFurnidataTranslateEnabled = adminAction(
{ permission: PERMS.ASSETS_IMPORT, schema: translateToggleSchema },
async (ctx) => {
const value = ctx.data.enabled ? "1" : "0";
await db
.insert(WebsiteSetting)
.values({ key: "furnidata_translate_enabled", value })
.onDuplicateKeyUpdate({ set: { value } });
await siteSettings.reload();
return actionOk({ enabled: ctx.data.enabled });
},
);
+4 -1
View File
@@ -4,6 +4,7 @@ import { buildFurniImportSource } from "@/lib/habbo-gamedata-hotel";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { getTreeFlat } from "@/lib/services/catalog-tree"; import { getTreeFlat } from "@/lib/services/catalog-tree";
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel"; import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
import { siteSettings } from "@/lib/services/site-settings";
export default async function StudioFurniPage(_props: { export default async function StudioFurniPage(_props: {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
@@ -13,15 +14,17 @@ export default async function StudioFurniPage(_props: {
redirect("/admin"); redirect("/admin");
} }
const [hotel, catalogTree] = await Promise.all([ const [hotel, catalogTree, translateEnabled] = await Promise.all([
getHabboGamedataHotel(), getHabboGamedataHotel(),
getTreeFlat(), getTreeFlat(),
siteSettings.getBool("furnidata_translate_enabled", true),
]); ]);
return ( return (
<StudioClient <StudioClient
source={buildFurniImportSource(hotel)} source={buildFurniImportSource(hotel)}
initialTree={catalogTree} initialTree={catalogTree}
defaultTranslate={translateEnabled}
/> />
); );
} }
+29 -6
View File
@@ -527,11 +527,24 @@ export const POST = withAdmin(
const { catalogNameFixed, haveOfferFixed, costCreditsFixed } = const { catalogNameFixed, haveOfferFixed, costCreditsFixed } =
await fixDatabaseConsistencyAfterImport(); await fixDatabaseConsistencyAfterImport();
// Rebuild all localized furnidata files (best effort) // Rebuild localized furnidata files (best effort). `translate: false`
// skips entirely; `translate: true` forces a build even when the global
// setting is off; undefined falls back to the global setting. An optional
// `langs` array restricts which languages are rebuilt.
let languages: Awaited<ReturnType<typeof buildLocalizedFurniDataFiles>> = let languages: Awaited<ReturnType<typeof buildLocalizedFurniDataFiles>> =
[]; [];
try { try {
languages = await buildLocalizedFurniDataFiles(); if (body.translate !== false) {
const langs = Array.isArray(body.langs)
? (body.langs as unknown[]).filter(
(l): l is string => typeof l === "string",
)
: undefined;
languages = await buildLocalizedFurniDataFiles(
body.translate === true,
langs,
);
}
} catch (err) { } catch (err) {
console.warn( console.warn(
"[import-furni] Localized furnidata build failed:", "[import-furni] Localized furnidata build failed:",
@@ -638,14 +651,24 @@ export const POST = withAdmin(
// Verify + fix interaction_modes_count (single consolidated pass) // Verify + fix interaction_modes_count (single consolidated pass)
const verification = await verifyAndFixInteractionModesCount(); const verification = await verifyAndFixInteractionModesCount();
// Patch localized furnidata files for this single furni (best effort) // Patch localized furnidata files for this single furni (best effort).
// Same `translate` / `langs` contract as the batch path above.
let languagesPatch: Awaited< let languagesPatch: Awaited<
ReturnType<typeof patchLocalizedFurniDataEntries> ReturnType<typeof patchLocalizedFurniDataEntries>
> = []; > = [];
try { try {
languagesPatch = await patchLocalizedFurniDataEntries([ if (body.translate !== false) {
{ classname, name, description: description ?? "" }, const langs = Array.isArray(body.langs)
]); ? (body.langs as unknown[]).filter(
(l): l is string => typeof l === "string",
)
: undefined;
languagesPatch = await patchLocalizedFurniDataEntries(
[{ classname, name, description: description ?? "" }],
body.translate === true,
langs,
);
}
} catch (err) { } catch (err) {
console.warn( console.warn(
"[import-furni] Localized patch failed:", "[import-furni] Localized patch failed:",
File diff suppressed because it is too large. Load diff
+13 -2
View File
@@ -433,6 +433,7 @@ function cloneFurniData(
*/ */
export async function buildLocalizedFurniDataFiles( export async function buildLocalizedFurniDataFiles(
force = false, force = false,
langs?: readonly string[],
): Promise<LanguageFileResult[]> { ): Promise<LanguageFileResult[]> {
if (!force && !(await translationEnabled())) { if (!force && !(await translationEnabled())) {
logger.debug( logger.debug(
@@ -440,6 +441,10 @@ export async function buildLocalizedFurniDataFiles(
); );
return []; return [];
} }
const langsToBuild =
langs && langs.length > 0
? FURNIDATA_LANGUAGES.filter((l) => langs.includes(l.lang))
: FURNIDATA_LANGUAGES;
return withFurniDataLock(async () => { return withFurniDataLock(async () => {
const master = (await readFurniData()) as Record< const master = (await readFurniData()) as Record<
string, string,
@@ -448,7 +453,7 @@ export async function buildLocalizedFurniDataFiles(
// Fetch all language maps in parallel (uses per-hotel memory cache). // Fetch all language maps in parallel (uses per-hotel memory cache).
const maps = await Promise.all( const maps = await Promise.all(
FURNIDATA_LANGUAGES.map(async ({ lang, hotel }) => { langsToBuild.map(async ({ lang, hotel }) => {
const map = await fetchTranslationsForHotel(hotel); const map = await fetchTranslationsForHotel(hotel);
return { lang, hotel, map }; return { lang, hotel, map };
}), }),
@@ -577,6 +582,7 @@ export async function buildLocalizedFurniDataFiles(
export async function patchLocalizedFurniDataEntries( export async function patchLocalizedFurniDataEntries(
entries: Array<{ classname: string; name?: string; description?: string }>, entries: Array<{ classname: string; name?: string; description?: string }>,
force = false, force = false,
langs?: readonly string[],
): Promise<LanguageFileResult[]> { ): Promise<LanguageFileResult[]> {
if (entries.length === 0) return []; if (entries.length === 0) return [];
if (!force && !(await translationEnabled())) { if (!force && !(await translationEnabled())) {
@@ -586,9 +592,14 @@ export async function patchLocalizedFurniDataEntries(
return []; return [];
} }
const langsToBuild =
langs && langs.length > 0
? FURNIDATA_LANGUAGES.filter((l) => langs.includes(l.lang))
: FURNIDATA_LANGUAGES;
return withFurniDataLock(async () => { return withFurniDataLock(async () => {
const maps = await Promise.all( const maps = await Promise.all(
FURNIDATA_LANGUAGES.map(async ({ lang, hotel }) => { langsToBuild.map(async ({ lang, hotel }) => {
const map = await fetchTranslationsForHotel(hotel); const map = await fetchTranslationsForHotel(hotel);
return { lang, hotel, map }; return { lang, hotel, map };
}), }),