feat: auto-translate furnidata per import (nl/en/de/fr/es/tr/it)
CI / check (push) Successful in 29s
CI / release (push) Skipped
CI / deploy (push) Successful in 54s

Generate localized FurnitureData_<lang>.json files from the master
FurnitureData.json + official Habbo hotel translations. Fetches per-hotel
furnidata with in-memory cache (6h TTL), maps by classname (* variant
aware), and overrides name/description where a translation exists.

Full rebuild after every batch/batch-regen/clone import and as admin
GET ?action=build-languages; single imports patch incrementally via
patchLocalizedFurniDataEntries. Failures are best-effort and never fail
the import itself. Files are written to every configured gamedata write
path (primary + mirrors) so /gamedata/config/FurnitureData_<lang>.json
is available for per-language Nitro clients.

New service: src/lib/services/furni-data-i18n.ts with
buildLocalizedFurniDataFiles, patchLocalizedFurniDataEntries,
FURNIDATA_LANGUAGES (nl/en/de/fr/es/tr/it) and applyTranslationsForTest.
This commit is contained in:
openhands committed 2026-08-21 14:16:37 +02:00
1 parent 984b5bf793
commit 8bb339cc49
5 files changed
+519

No files matched your search

@@ -14,6 +14,7 @@ import {
reconcileFurniDataWithItemsBase,
verifyAndFixSpriteIds,
} from "@/lib/services/furni-data";
import { buildLocalizedFurniDataFiles } from "@/lib/services/furni-data-i18n";
import {
ensureFurniOwnership,
reconcileImportedOfferIds,
@@ -147,6 +148,11 @@ export const POST = withAdmin(
await rebuildCatalogOfferIds();
await verifyAndFixSpriteIds();
await verifyAndFixInteractionModesCount();
try {
await buildLocalizedFurniDataFiles();
} catch {
/* localized build failures are non-fatal */
}
await ensureFurniOwnership();
await syncAssetsToGamedataBundle();
await rcon.updateCatalog();
@@ -14,6 +14,7 @@ import {
reconcileFurniDataWithItemsBase,
verifyAndFixSpriteIds,
} from "@/lib/services/furni-data";
import { buildLocalizedFurniDataFiles } from "@/lib/services/furni-data-i18n";
import {
downloadFile,
ensureDirectories,
@@ -277,6 +278,18 @@ export const POST = withAdmin(
// Repair any sprite_id drift so the client renders the right item
const spriteVerify = await verifyAndFixSpriteIds();
let languages: Awaited<
ReturnType<typeof buildLocalizedFurniDataFiles>
> = [];
try {
languages = await buildLocalizedFurniDataFiles();
} catch (err) {
console.warn(
"[batch-regen] Localized furnidata build failed:",
(err as Error).message,
);
}
send({
type: "regen_complete",
succeeded,
@@ -293,6 +306,7 @@ export const POST = withAdmin(
offerRebuildFixed: offerRebuild.fixed,
spriteChecked: spriteVerify.checked,
spriteFixed: spriteVerify.fixed,
languages,
duration: Date.now() - startTime,
});
@@ -9,6 +9,7 @@ import {
reconcileFurniDataWithItemsBase,
verifyAndFixSpriteIds,
} from "@/lib/services/furni-data";
import { buildLocalizedFurniDataFiles } from "@/lib/services/furni-data-i18n";
import {
ensureDirectories,
ensureFurniOwnership,
@@ -232,6 +233,9 @@ export const POST = withAdmin(
let offerRebuildFixed = 0;
let spriteFixed = 0;
let interactionFixed = 0;
let languages: Awaited<
ReturnType<typeof buildLocalizedFurniDataFiles>
> = [];
try {
offerIdsFixed = (await reconcileImportedOfferIds()).fixed;
ownershipFixed = (await ensureFurniOwnership()).fixed;
@@ -246,6 +250,14 @@ export const POST = withAdmin(
offerRebuildFixed = (await rebuildCatalogOfferIds()).fixed;
spriteFixed = (await verifyAndFixSpriteIds()).fixed;
interactionFixed = (await verifyAndFixInteractionModesCount()).fixed;
try {
languages = await buildLocalizedFurniDataFiles();
} catch (err) {
console.warn(
"[import-furni] Localized furnidata build failed:",
(err as Error).message,
);
}
} catch (err) {
console.warn(
"[import-furni] Post-import reconcile/ownership failed:",
@@ -269,6 +281,7 @@ export const POST = withAdmin(
offerRebuildFixed,
spriteFixed,
interactionFixed,
languages,
duration: Date.now() - startTime,
});
+38
View File
@@ -14,6 +14,10 @@ import {
reconcileFurniDataWithItemsBase,
verifyAndFixSpriteIds,
} from "@/lib/services/furni-data";
import {
buildLocalizedFurniDataFiles,
patchLocalizedFurniDataEntries,
} from "@/lib/services/furni-data-i18n";
import {
autoPriceFurni,
CATEGORY_PAGE,
@@ -189,6 +193,11 @@ export const GET = withAdmin(
});
}
if (action === "build-languages") {
const languages = await buildLocalizedFurniDataFiles();
return apiOk({ languages });
}
// Missing nitro endpoint: returns only imported items missing .nitro files
if (action === "missing-nitro") {
const pageNum = parseInt(
@@ -471,6 +480,18 @@ export const POST = withAdmin(
// Verify + fix interaction_modes_count once for the whole batch
const verification = await verifyAndFixInteractionModesCount();
// Rebuild all localized furnidata files (best effort)
let languages: Awaited<ReturnType<typeof buildLocalizedFurniDataFiles>> =
[];
try {
languages = await buildLocalizedFurniDataFiles();
} catch (err) {
console.warn(
"[import-furni] Localized furnidata build failed:",
(err as Error).message,
);
}
const succeeded = results.filter((r) => r.ok).length;
const failed = results.filter((r) => !r.ok).length;
@@ -492,6 +513,7 @@ export const POST = withAdmin(
spriteChecked: spriteVerify.checked,
spriteFixed: spriteVerify.fixed,
interactionFixed: verification.fixed,
languages,
});
}
@@ -562,6 +584,21 @@ export const POST = withAdmin(
// Verify + fix interaction_modes_count (single consolidated pass)
const verification = await verifyAndFixInteractionModesCount();
// Patch localized furnidata files for this single furni (best effort)
let languagesPatch: Awaited<
ReturnType<typeof patchLocalizedFurniDataEntries>
> = [];
try {
languagesPatch = await patchLocalizedFurniDataEntries([
{ classname, name, description: description ?? "" },
]);
} catch (err) {
console.warn(
"[import-furni] Localized patch failed:",
(err as Error).message,
);
}
logAudit({
userId: ctx.session.user.id,
action: "furni_import",
@@ -595,6 +632,7 @@ export const POST = withAdmin(
spriteChecked: spriteVerify.checked,
spriteFixed: spriteVerify.fixed,
interactionFixed: verification.fixed,
languages: languagesPatch,
spriteCount: result.spriteCount,
warnings: result.warnings,
});