perf: offload per-import localized patch to worker + cache FurnitureData parse
Per single furni import, patchLocalizedFurniDataEntries ran for every language and did a full 100k-entry JSON.parse + scan + JSON.stringify on the main thread. That CPU spike is now offloaded to the translation worker (init/prepare/finalize reuses the same pure core helpers), so importing never blocks the event loop. The main thread only does async file I/O and the network LibreTranslate calls. Falls back to the same helpers on the main thread if no worker is available. Also cache readFurniData() by file mtime+size so the 100k-entry JSON is parsed once per change instead of on every import/fix/reconcile read (invalidated on write).
This commit is contained in:
1 parent
6ed1b03e24
commit
6ae7c09682
5 files changed
+502
-226
No files matched your search
@@ -153,3 +153,242 @@ export function finalizeLangFile(
|
|||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Localized patch (single-item import path) */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
export interface PatchEntry {
|
||||||
|
classname: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatchRef {
|
||||||
|
entry: FurniDataEntry;
|
||||||
|
field: "name" | "description";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatchNewItem {
|
||||||
|
section: "roomitemtypes" | "wallitemtypes";
|
||||||
|
found: FurniDataEntry;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
translateName: boolean;
|
||||||
|
translateDesc: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreparedPatch {
|
||||||
|
data: FurniDataMaster;
|
||||||
|
refs: PatchRef[];
|
||||||
|
newItems: PatchNewItem[];
|
||||||
|
pendingTexts: Set<string>;
|
||||||
|
officialPatched: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEntryInFurniData(
|
||||||
|
data: FurniDataMaster,
|
||||||
|
classname: string,
|
||||||
|
): {
|
||||||
|
entry: FurniDataEntry;
|
||||||
|
section: "roomitemtypes" | "wallitemtypes";
|
||||||
|
} | null {
|
||||||
|
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||||
|
const hit = data[section]?.furnitype.find((e) => e.classname === classname);
|
||||||
|
if (hit) return { entry: hit, section };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass 1 of a localized patch: load the existing language file (or clone the
|
||||||
|
* master when it does not exist yet), touch ONLY the wanted classnames, and
|
||||||
|
* collect the texts that still need LibreTranslate. The official-vs-Libre
|
||||||
|
* decision, master clone and the full file scan all run here (CPU-heavy) so the
|
||||||
|
* network translation step can run afterwards on the main thread.
|
||||||
|
*/
|
||||||
|
export function preparePatch(
|
||||||
|
fileContent: string | null,
|
||||||
|
master: FurniDataMaster,
|
||||||
|
lang: string,
|
||||||
|
hotel: HabboGamedataHotel,
|
||||||
|
officialMap: Map<string, Translation>,
|
||||||
|
entries: PatchEntry[],
|
||||||
|
): PreparedPatch {
|
||||||
|
const officialIsReal = officialIsRealTranslation(lang, hotel);
|
||||||
|
let data: FurniDataMaster;
|
||||||
|
try {
|
||||||
|
data = fileContent
|
||||||
|
? (JSON.parse(fileContent) as FurniDataMaster)
|
||||||
|
: cloneFurniData(master);
|
||||||
|
if (!fileContent && officialIsReal) {
|
||||||
|
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||||
|
for (const entry of data[section]?.furnitype ?? []) {
|
||||||
|
const cn = typeof entry.classname === "string" ? entry.classname : "";
|
||||||
|
if (!cn) continue;
|
||||||
|
const t = lookupTranslation(officialMap, cn);
|
||||||
|
if (t?.name) entry.name = t.name;
|
||||||
|
if (t?.description) entry.description = t.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
data = cloneFurniData(master);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wanted = new Map(entries.map((e) => [e.classname, e]));
|
||||||
|
const wantedSet = new Set(wanted.keys());
|
||||||
|
const refs: PatchRef[] = [];
|
||||||
|
const pendingTexts = new Set<string>();
|
||||||
|
let officialPatched = 0;
|
||||||
|
|
||||||
|
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||||
|
for (const entry of data[section]?.furnitype ?? []) {
|
||||||
|
const cn = typeof entry.classname === "string" ? entry.classname : "";
|
||||||
|
if (!cn || !wantedSet.has(cn)) continue;
|
||||||
|
const wantedEntry = wanted.get(cn);
|
||||||
|
if (!wantedEntry) continue;
|
||||||
|
const t = lookupTranslation(officialMap, cn);
|
||||||
|
const origName =
|
||||||
|
wantedEntry.name ?? (typeof entry.name === "string" ? entry.name : "");
|
||||||
|
const origDesc =
|
||||||
|
wantedEntry.description ??
|
||||||
|
(typeof entry.description === "string" ? entry.description : "");
|
||||||
|
let didPatch = false;
|
||||||
|
if (officialIsReal && t?.name) {
|
||||||
|
entry.name = t.name;
|
||||||
|
didPatch = true;
|
||||||
|
} else if (lang !== "en" && origName) {
|
||||||
|
pendingTexts.add(origName);
|
||||||
|
refs.push({ entry, field: "name" });
|
||||||
|
}
|
||||||
|
if (officialIsReal && t?.description) {
|
||||||
|
entry.description = t.description;
|
||||||
|
didPatch = true;
|
||||||
|
} else if (lang !== "en" && origDesc) {
|
||||||
|
pendingTexts.add(origDesc);
|
||||||
|
refs.push({ entry, field: "description" });
|
||||||
|
}
|
||||||
|
if (didPatch) officialPatched++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New classnames not yet in the file: copy from master with translation applied.
|
||||||
|
const existingClassnames = new Set<string>();
|
||||||
|
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||||
|
for (const e of data[section]?.furnitype ?? []) {
|
||||||
|
const cn = typeof e?.classname === "string" ? e.classname : "";
|
||||||
|
if (cn) existingClassnames.add(cn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newItems: PatchNewItem[] = [];
|
||||||
|
const newTexts = new Set<string>();
|
||||||
|
for (const { classname, name, description } of entries) {
|
||||||
|
if (existingClassnames.has(classname)) continue;
|
||||||
|
const found = findEntryInFurniData(master, classname);
|
||||||
|
if (!found) continue;
|
||||||
|
const t = lookupTranslation(officialMap, classname);
|
||||||
|
const srcName =
|
||||||
|
name ?? (typeof found.entry.name === "string" ? found.entry.name : "");
|
||||||
|
const srcDesc =
|
||||||
|
description ??
|
||||||
|
(typeof found.entry.description === "string"
|
||||||
|
? found.entry.description
|
||||||
|
: "");
|
||||||
|
let finalName: string | undefined;
|
||||||
|
let finalDesc: string | undefined;
|
||||||
|
let translateName = false;
|
||||||
|
let translateDesc = false;
|
||||||
|
if (officialIsReal && t?.name) finalName = t.name;
|
||||||
|
else if (lang !== "en" && srcName) {
|
||||||
|
finalName = srcName;
|
||||||
|
translateName = true;
|
||||||
|
newTexts.add(srcName);
|
||||||
|
} else finalName = srcName;
|
||||||
|
if (officialIsReal && t?.description) finalDesc = t.description;
|
||||||
|
else if (lang !== "en" && srcDesc) {
|
||||||
|
finalDesc = srcDesc;
|
||||||
|
translateDesc = true;
|
||||||
|
newTexts.add(srcDesc);
|
||||||
|
} else finalDesc = srcDesc;
|
||||||
|
newItems.push({
|
||||||
|
section: found.section,
|
||||||
|
found: found.entry,
|
||||||
|
name: finalName,
|
||||||
|
description: finalDesc,
|
||||||
|
translateName,
|
||||||
|
translateDesc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const tx of newTexts) pendingTexts.add(tx);
|
||||||
|
|
||||||
|
return { data, refs, newItems, pendingTexts, officialPatched };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinalizedPatch {
|
||||||
|
json: string;
|
||||||
|
patched: number;
|
||||||
|
added: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass 2 of a localized patch: apply the (network) LibreTranslate results to the
|
||||||
|
* prepared clone and serialize it. New items are appended from the master copy.
|
||||||
|
*/
|
||||||
|
export function finalizePatch(
|
||||||
|
prepared: PreparedPatch,
|
||||||
|
translations: Map<string, string>,
|
||||||
|
): FinalizedPatch {
|
||||||
|
const { data, refs, newItems, officialPatched } = prepared;
|
||||||
|
|
||||||
|
const counted = new Set<FurniDataEntry>();
|
||||||
|
let librePatched = 0;
|
||||||
|
for (const ref of refs) {
|
||||||
|
const original = ref.entry[ref.field];
|
||||||
|
if (typeof original !== "string") continue;
|
||||||
|
const translated = translations.get(original.trim());
|
||||||
|
if (translated === undefined || translated === original) continue;
|
||||||
|
ref.entry[ref.field] = translated;
|
||||||
|
if (!counted.has(ref.entry)) {
|
||||||
|
counted.add(ref.entry);
|
||||||
|
librePatched++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
for (const item of newItems) {
|
||||||
|
const clone = { ...item.found } as FurniDataEntry;
|
||||||
|
if (item.name !== undefined) {
|
||||||
|
if (item.translateName) {
|
||||||
|
const tr = translations.get(item.name.trim());
|
||||||
|
clone.name = tr && tr !== item.name ? tr : item.name;
|
||||||
|
} else {
|
||||||
|
clone.name = item.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.description !== undefined) {
|
||||||
|
if (item.translateDesc) {
|
||||||
|
const tr = translations.get(item.description.trim());
|
||||||
|
clone.description =
|
||||||
|
tr && tr !== item.description ? tr : item.description;
|
||||||
|
} else {
|
||||||
|
clone.description = item.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!data[item.section]) data[item.section] = { furnitype: [] };
|
||||||
|
data[item.section].furnitype.push(clone);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total =
|
||||||
|
(data.roomitemtypes?.furnitype?.length ?? 0) +
|
||||||
|
(data.wallitemtypes?.furnitype?.length ?? 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: JSON.stringify(data),
|
||||||
|
patched: officialPatched + librePatched + added,
|
||||||
|
added,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -69,8 +69,6 @@ export interface LanguageFileResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { Translation } from "./furni-data-i18n-core";
|
|
||||||
|
|
||||||
const LANG_CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours — translations rarely change
|
const LANG_CACHE_TTL = 6 * 60 * 60 * 1000; // 6 hours — translations rarely change
|
||||||
|
|
||||||
interface LangCacheEntry {
|
interface LangCacheEntry {
|
||||||
@@ -257,7 +255,12 @@ async function translateManyWithLibre(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { FurniDataMaster, PreparedLang } from "./furni-data-i18n-core";
|
import type {
|
||||||
|
FurniDataMaster,
|
||||||
|
PreparedLang,
|
||||||
|
PreparedPatch,
|
||||||
|
Translation,
|
||||||
|
} from "./furni-data-i18n-core";
|
||||||
/**
|
/**
|
||||||
* The pure clone / scan / stringify helpers live in furni-data-i18n-core so they
|
* The pure clone / scan / stringify helpers live in furni-data-i18n-core so they
|
||||||
* can run inside a worker thread (that module has no server-only / network deps).
|
* can run inside a worker thread (that module has no server-only / network deps).
|
||||||
@@ -265,25 +268,26 @@ import type { FurniDataMaster, PreparedLang } from "./furni-data-i18n-core";
|
|||||||
* backward compatibility.
|
* backward compatibility.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
cloneFurniData,
|
|
||||||
finalizeLangFile,
|
finalizeLangFile,
|
||||||
|
finalizePatch,
|
||||||
lookupTranslation,
|
lookupTranslation,
|
||||||
officialIsRealTranslation,
|
|
||||||
prepareLangFile,
|
prepareLangFile,
|
||||||
|
preparePatch,
|
||||||
} from "./furni-data-i18n-core";
|
} from "./furni-data-i18n-core";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
FurniDataMaster,
|
FurniDataMaster,
|
||||||
|
PatchEntry,
|
||||||
PreparedLang,
|
PreparedLang,
|
||||||
|
PreparedPatch,
|
||||||
Translation,
|
Translation,
|
||||||
} from "./furni-data-i18n-core";
|
} from "./furni-data-i18n-core";
|
||||||
export {
|
export {
|
||||||
cloneFurniData,
|
|
||||||
lookupTranslation,
|
lookupTranslation,
|
||||||
officialIsRealTranslation,
|
officialIsRealTranslation,
|
||||||
} from "./furni-data-i18n-core";
|
} from "./furni-data-i18n-core";
|
||||||
|
|
||||||
async function fetchTranslationsForHotel(
|
export async function fetchTranslationsForHotel(
|
||||||
hotel: HabboGamedataHotel,
|
hotel: HabboGamedataHotel,
|
||||||
): Promise<Map<string, Translation>> {
|
): Promise<Map<string, Translation>> {
|
||||||
const cached = langCache.get(hotel);
|
const cached = langCache.get(hotel);
|
||||||
@@ -400,16 +404,12 @@ export async function buildLocalizedFurniDataFiles(
|
|||||||
{ furnitype: Array<Record<string, unknown>> }
|
{ furnitype: Array<Record<string, unknown>> }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// Fetch each hotel's official translations once (network + per-hotel cache).
|
|
||||||
const hotels = [...new Set(langsToBuild.map((l) => l.hotel))];
|
|
||||||
const hotelMaps: Record<string, Map<string, Translation>> = {};
|
|
||||||
for (const hotel of hotels) {
|
|
||||||
hotelMaps[hotel] = await fetchTranslationsForHotel(hotel);
|
|
||||||
}
|
|
||||||
|
|
||||||
const worker = getTranslationWorker();
|
const worker = getTranslationWorker();
|
||||||
if (worker) {
|
if (worker) {
|
||||||
await worker.init(JSON.stringify(master), hotelMaps);
|
// Reads the master + official maps once on the main thread (cached
|
||||||
|
// per process) and ships them to the worker so the per-language
|
||||||
|
// clone/scan/stringify runs off the main thread.
|
||||||
|
await worker.ensureInit();
|
||||||
}
|
}
|
||||||
// Main-thread fallback store, used only when no worker is available.
|
// Main-thread fallback store, used only when no worker is available.
|
||||||
const localPrepared = new Map<string, PreparedLang>();
|
const localPrepared = new Map<string, PreparedLang>();
|
||||||
@@ -430,11 +430,12 @@ export async function buildLocalizedFurniDataFiles(
|
|||||||
if (worker) {
|
if (worker) {
|
||||||
pendingTexts = await worker.prepare(lang, hotel);
|
pendingTexts = await worker.prepare(lang, hotel);
|
||||||
} else {
|
} else {
|
||||||
|
const map = await fetchTranslationsForHotel(hotel);
|
||||||
const r = prepareLangFile(
|
const r = prepareLangFile(
|
||||||
master as unknown as FurniDataMaster,
|
master as unknown as FurniDataMaster,
|
||||||
lang,
|
lang,
|
||||||
hotel,
|
hotel,
|
||||||
hotelMaps[hotel] ?? new Map(),
|
map,
|
||||||
);
|
);
|
||||||
pendingTexts = r.pendingTexts;
|
pendingTexts = r.pendingTexts;
|
||||||
preparedKey = lang;
|
preparedKey = lang;
|
||||||
@@ -531,220 +532,95 @@ export async function patchLocalizedFurniDataEntries(
|
|||||||
: FURNIDATA_LANGUAGES;
|
: FURNIDATA_LANGUAGES;
|
||||||
|
|
||||||
return withFurniDataLock(async () => {
|
return withFurniDataLock(async () => {
|
||||||
// Read the master once and reuse it for the fallback clone and for
|
const worker = getTranslationWorker();
|
||||||
// looking up brand-new classnames — avoids parsing FurnitureData twice.
|
// Fallback store for when there is no worker (test env / spawn failure).
|
||||||
const master = (await readFurniData()) as Record<
|
const localPrepared = new Map<string, PreparedPatch>();
|
||||||
string,
|
|
||||||
{ furnitype: Array<Record<string, unknown>> }
|
|
||||||
>;
|
|
||||||
const maps = await Promise.all(
|
|
||||||
langsToBuild.map(async ({ lang, hotel }) => {
|
|
||||||
const map = await fetchTranslationsForHotel(hotel);
|
|
||||||
return { lang, hotel, map };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const wanted = new Map<string, { name?: string; description?: string }>();
|
|
||||||
for (const e of entries) wanted.set(e.classname, e);
|
|
||||||
const wantedSet = new Set(wanted.keys());
|
|
||||||
|
|
||||||
const results: LanguageFileResult[] = [];
|
const results: LanguageFileResult[] = [];
|
||||||
|
|
||||||
for (const { lang, hotel, map } of maps) {
|
for (const { lang, hotel } of langsToBuild) {
|
||||||
try {
|
try {
|
||||||
const officialIsReal = officialIsRealTranslation(lang, hotel);
|
|
||||||
const targets = await getLocalizedWritePaths(lang);
|
const targets = await getLocalizedWritePaths(lang);
|
||||||
let patched = 0;
|
|
||||||
|
|
||||||
// Load the existing language file if present, otherwise build it
|
// Read the existing language file as raw text. The expensive
|
||||||
// from master (official translations only — never LibreTranslate,
|
// parse + scan + stringify all happen in the worker, so the main
|
||||||
// to avoid 100k+ network calls on first creation).
|
// thread only does async file I/O here.
|
||||||
let data: Record<string, { furnitype: Array<Record<string, unknown>> }>;
|
let fileContent: string | null = null;
|
||||||
try {
|
try {
|
||||||
const raw = await fs.readFile(targets[0], "utf-8");
|
fileContent = await fs.readFile(targets[0], "utf-8");
|
||||||
data = JSON.parse(raw);
|
|
||||||
} catch {
|
} catch {
|
||||||
data = cloneFurniData(master);
|
fileContent = null;
|
||||||
if (officialIsReal) {
|
|
||||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
|
||||||
for (const entry of data[section]?.furnitype ?? []) {
|
|
||||||
const cn =
|
|
||||||
typeof entry.classname === "string" ? entry.classname : "";
|
|
||||||
if (!cn) continue;
|
|
||||||
const t = lookupTranslation(map, cn);
|
|
||||||
if (t?.name) entry.name = t.name;
|
|
||||||
if (t?.description) entry.description = t.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1 — touch ONLY the wanted classnames (most patches are a
|
let pendingTexts: string[];
|
||||||
// single import), collecting the texts that need LibreTranslate.
|
let localKey: string | null = null;
|
||||||
const refs: Array<{
|
try {
|
||||||
entry: Record<string, unknown>;
|
if (!worker) throw new Error("no translation worker");
|
||||||
field: "name" | "description";
|
pendingTexts = await worker.patchPrepare(
|
||||||
}> = [];
|
fileContent,
|
||||||
const pendingTexts = new Set<string>();
|
lang,
|
||||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
hotel,
|
||||||
for (const entry of data[section]?.furnitype ?? []) {
|
entries,
|
||||||
const cn =
|
);
|
||||||
typeof entry.classname === "string" ? entry.classname : "";
|
} catch {
|
||||||
if (!cn || !wantedSet.has(cn)) continue;
|
// Main-thread fallback: run the exact same pure helpers here.
|
||||||
const wantedEntry = wanted.get(cn);
|
const master = (await readFurniData()) as unknown as FurniDataMaster;
|
||||||
if (!wantedEntry) continue;
|
const map = await fetchTranslationsForHotel(hotel);
|
||||||
const t = lookupTranslation(map, cn);
|
const r = preparePatch(
|
||||||
let didPatch = false;
|
fileContent,
|
||||||
const origName =
|
master,
|
||||||
wantedEntry.name ??
|
lang,
|
||||||
(typeof entry.name === "string" ? entry.name : "");
|
hotel,
|
||||||
const origDesc =
|
map,
|
||||||
wantedEntry.description ??
|
entries,
|
||||||
(typeof entry.description === "string" ? entry.description : "");
|
);
|
||||||
if (officialIsReal && t?.name) {
|
localPrepared.set(lang, r);
|
||||||
entry.name = t.name;
|
pendingTexts = [...r.pendingTexts];
|
||||||
didPatch = true;
|
localKey = lang;
|
||||||
} else if (lang !== "en" && origName) {
|
|
||||||
pendingTexts.add(origName);
|
|
||||||
refs.push({ entry, field: "name" });
|
|
||||||
}
|
|
||||||
if (officialIsReal && t?.description) {
|
|
||||||
entry.description = t.description;
|
|
||||||
didPatch = true;
|
|
||||||
} else if (lang !== "en" && origDesc) {
|
|
||||||
pendingTexts.add(origDesc);
|
|
||||||
refs.push({ entry, field: "description" });
|
|
||||||
}
|
|
||||||
if (didPatch) patched++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch-translate (dedup + concurrency-capped) instead of one
|
// Batch-translate (network, async — never blocks the main thread).
|
||||||
// HTTP call per text.
|
const translations = await translateManyWithLibre(
|
||||||
const translations = await translateManyWithLibre(pendingTexts, lang);
|
new Set(pendingTexts),
|
||||||
const counted = new Set<Record<string, unknown>>();
|
lang,
|
||||||
for (const ref of refs) {
|
);
|
||||||
const original = ref.entry[ref.field];
|
|
||||||
if (typeof original !== "string") continue;
|
let json: string;
|
||||||
const translated = translations.get(original.trim());
|
let patched = 0;
|
||||||
if (translated === undefined || translated === original) continue;
|
let added = 0;
|
||||||
ref.entry[ref.field] = translated;
|
let total = 0;
|
||||||
if (!counted.has(ref.entry)) {
|
if (worker && localKey === null) {
|
||||||
counted.add(ref.entry);
|
const res = await worker.patchFinalize(lang, translations);
|
||||||
patched++;
|
json = res.json;
|
||||||
}
|
patched = res.patched;
|
||||||
|
added = res.added;
|
||||||
|
total = res.total;
|
||||||
|
} else {
|
||||||
|
const r = localPrepared.get(localKey!)!;
|
||||||
|
const res = finalizePatch(r, translations);
|
||||||
|
json = res.json;
|
||||||
|
patched = res.patched;
|
||||||
|
added = res.added;
|
||||||
|
total = res.total;
|
||||||
|
localPrepared.delete(localKey!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New classnames not yet in the file: copy from master with
|
|
||||||
// translation applied (only for the wanted set).
|
|
||||||
const existingClassnames = new Set<string>();
|
|
||||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
|
||||||
for (const e of data[section]?.furnitype ?? []) {
|
|
||||||
const cn = typeof e?.classname === "string" ? e.classname : "";
|
|
||||||
if (cn) existingClassnames.add(cn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newItems: Array<{
|
|
||||||
section: "roomitemtypes" | "wallitemtypes";
|
|
||||||
found: Record<string, unknown>;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
translateName: boolean;
|
|
||||||
translateDesc: boolean;
|
|
||||||
}> = [];
|
|
||||||
const newTexts = new Set<string>();
|
|
||||||
for (const { classname, name, description } of entries) {
|
|
||||||
if (existingClassnames.has(classname)) continue;
|
|
||||||
let found: Record<string, unknown> | undefined;
|
|
||||||
let foundSection: "roomitemtypes" | "wallitemtypes" | undefined;
|
|
||||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
|
||||||
const hit = (
|
|
||||||
master[section]?.furnitype as
|
|
||||||
| Array<Record<string, unknown>>
|
|
||||||
| undefined
|
|
||||||
)?.find((e) => e.classname === classname) as
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
if (hit) {
|
|
||||||
found = hit;
|
|
||||||
foundSection = section;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found || !foundSection) continue;
|
|
||||||
const t = lookupTranslation(map, classname);
|
|
||||||
const srcName =
|
|
||||||
name ?? (typeof found.name === "string" ? found.name : "");
|
|
||||||
const srcDesc =
|
|
||||||
description ??
|
|
||||||
(typeof found.description === "string" ? found.description : "");
|
|
||||||
let finalName: string | undefined;
|
|
||||||
let finalDesc: string | undefined;
|
|
||||||
let translateName = false;
|
|
||||||
let translateDesc = false;
|
|
||||||
if (officialIsReal && t?.name) finalName = t.name;
|
|
||||||
else if (lang !== "en" && srcName) {
|
|
||||||
finalName = srcName;
|
|
||||||
translateName = true;
|
|
||||||
newTexts.add(srcName);
|
|
||||||
} else finalName = srcName;
|
|
||||||
if (officialIsReal && t?.description) finalDesc = t.description;
|
|
||||||
else if (lang !== "en" && srcDesc) {
|
|
||||||
finalDesc = srcDesc;
|
|
||||||
translateDesc = true;
|
|
||||||
newTexts.add(srcDesc);
|
|
||||||
} else finalDesc = srcDesc;
|
|
||||||
newItems.push({
|
|
||||||
section: foundSection,
|
|
||||||
found,
|
|
||||||
name: finalName,
|
|
||||||
description: finalDesc,
|
|
||||||
translateName,
|
|
||||||
translateDesc,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const newTranslations = await translateManyWithLibre(newTexts, lang);
|
|
||||||
for (const item of newItems) {
|
|
||||||
const clone = { ...item.found } as Record<string, unknown>;
|
|
||||||
if (item.name !== undefined) {
|
|
||||||
if (item.translateName) {
|
|
||||||
const tr = newTranslations.get(item.name.trim());
|
|
||||||
clone.name = tr && tr !== item.name ? tr : item.name;
|
|
||||||
} else {
|
|
||||||
clone.name = item.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.description !== undefined) {
|
|
||||||
if (item.translateDesc) {
|
|
||||||
const tr = newTranslations.get(item.description.trim());
|
|
||||||
clone.description =
|
|
||||||
tr && tr !== item.description ? tr : item.description;
|
|
||||||
} else {
|
|
||||||
clone.description = item.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!data[item.section]) data[item.section] = { furnitype: [] };
|
|
||||||
data[item.section].furnitype.push(clone);
|
|
||||||
patched++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lang !== "en") await saveLibreCache();
|
|
||||||
|
|
||||||
const payload = JSON.stringify(data);
|
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||||
const tmp = `${target}.tmp`;
|
const tmp = `${target}.tmp`;
|
||||||
await fs.writeFile(tmp, payload, "utf-8");
|
await fs.writeFile(tmp, json, "utf-8");
|
||||||
await fs.rename(tmp, target);
|
await fs.rename(tmp, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lang !== "en") await saveLibreCache();
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
lang,
|
lang,
|
||||||
hotel,
|
hotel,
|
||||||
file: targets[0] ?? `FurnitureData_${lang}.json`,
|
file: targets[0] ?? `FurnitureData_${lang}.json`,
|
||||||
translatedRoom: patched,
|
translatedRoom: patched,
|
||||||
translatedWall: 0,
|
translatedWall: added,
|
||||||
total: patched,
|
total,
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -199,10 +199,34 @@ export async function withFurniDataLock<T>(fn: () => Promise<T>): Promise<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedFurniData: {
|
||||||
|
path: string;
|
||||||
|
mtimeMs: number;
|
||||||
|
size: number;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
export async function readFurniData(): Promise<Record<string, unknown>> {
|
export async function readFurniData(): Promise<Record<string, unknown>> {
|
||||||
const target = await getFurnitureDataReadPath();
|
const target = await getFurnitureDataReadPath();
|
||||||
|
const stat = await fs.stat(target);
|
||||||
|
if (
|
||||||
|
cachedFurniData &&
|
||||||
|
cachedFurniData.path === target &&
|
||||||
|
cachedFurniData.mtimeMs === stat.mtimeMs &&
|
||||||
|
cachedFurniData.size === stat.size
|
||||||
|
) {
|
||||||
|
// File unchanged since last parse: skip the (expensive) full JSON.parse.
|
||||||
|
return cachedFurniData.data;
|
||||||
|
}
|
||||||
const raw = await fs.readFile(/*turbopackIgnore: true*/ target, "utf-8");
|
const raw = await fs.readFile(/*turbopackIgnore: true*/ target, "utf-8");
|
||||||
return JSON.parse(raw);
|
const data = JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
cachedFurniData = {
|
||||||
|
path: target,
|
||||||
|
mtimeMs: stat.mtimeMs,
|
||||||
|
size: stat.size,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeFurniData(
|
export async function writeFurniData(
|
||||||
@@ -216,6 +240,9 @@ export async function writeFurniData(
|
|||||||
await fs.writeFile(/*turbopackIgnore: true*/ tmp, payload, "utf-8");
|
await fs.writeFile(/*turbopackIgnore: true*/ tmp, payload, "utf-8");
|
||||||
await fs.rename(/*turbopackIgnore: true*/ tmp, target);
|
await fs.rename(/*turbopackIgnore: true*/ tmp, target);
|
||||||
}
|
}
|
||||||
|
// The file just changed: force the next read to reparse instead of serving
|
||||||
|
// a possibly-mutated cached object.
|
||||||
|
cachedFurniData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildFurniEntry(params: {
|
export function buildFurniEntry(params: {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Worker } from "node:worker_threads";
|
import { Worker } from "node:worker_threads";
|
||||||
import type { Translation } from "./furni-data-i18n-core";
|
import type { PatchEntry, Translation } from "./furni-data-i18n-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the CPU-heavy FurnitureData localization (deep clone + 100k-entry scans +
|
* Runs the CPU-heavy FurnitureData localization (deep clone + 100k-entry scans +
|
||||||
* JSON.stringify) in a single worker thread so the main Node event loop stays
|
* JSON.stringify) in a single worker thread so the main Node event loop stays
|
||||||
* free during a "translate everything" build. Network/DB work (fetching official
|
* free during a "translate everything" build and during per-import patches.
|
||||||
* Habbo translations, calling LibreTranslate, writing files) stays on the main
|
* Network/DB work (fetching official Habbo translations, calling LibreTranslate,
|
||||||
* thread; only the synchronous CPU is offloaded.
|
* writing files) stays on the main thread; only the synchronous CPU is offloaded.
|
||||||
*
|
*
|
||||||
* Degrades gracefully: in the test environment (or if the worker can't be
|
* Degrades gracefully: in the test environment (or if the worker can't be
|
||||||
* created) getTranslationWorker() returns null and callers run the same pure
|
* created) getTranslationWorker() returns null and callers run the same pure
|
||||||
@@ -30,11 +30,32 @@ interface FinalizeReq {
|
|||||||
lang: string;
|
lang: string;
|
||||||
translations: Map<string, string>;
|
translations: Map<string, string>;
|
||||||
}
|
}
|
||||||
type Req = InitReq | PrepareReq | FinalizeReq;
|
interface PatchPrepareReq {
|
||||||
|
id: number;
|
||||||
|
type: "patchPrepare";
|
||||||
|
lang: string;
|
||||||
|
hotel: string;
|
||||||
|
fileContent: string | null;
|
||||||
|
entries: PatchEntry[];
|
||||||
|
}
|
||||||
|
interface PatchFinalizeReq {
|
||||||
|
id: number;
|
||||||
|
type: "patchFinalize";
|
||||||
|
lang: string;
|
||||||
|
translations: Map<string, string>;
|
||||||
|
}
|
||||||
|
type Req =
|
||||||
|
| InitReq
|
||||||
|
| PrepareReq
|
||||||
|
| FinalizeReq
|
||||||
|
| PatchPrepareReq
|
||||||
|
| PatchFinalizeReq;
|
||||||
type ReqWithoutId =
|
type ReqWithoutId =
|
||||||
| Omit<InitReq, "id">
|
| Omit<InitReq, "id">
|
||||||
| Omit<PrepareReq, "id">
|
| Omit<PrepareReq, "id">
|
||||||
| Omit<FinalizeReq, "id">;
|
| Omit<FinalizeReq, "id">
|
||||||
|
| Omit<PatchPrepareReq, "id">
|
||||||
|
| Omit<PatchFinalizeReq, "id">;
|
||||||
|
|
||||||
type Res =
|
type Res =
|
||||||
| {
|
| {
|
||||||
@@ -44,12 +65,15 @@ type Res =
|
|||||||
json?: string;
|
json?: string;
|
||||||
translatedRoom?: number;
|
translatedRoom?: number;
|
||||||
translatedWall?: number;
|
translatedWall?: number;
|
||||||
|
patched?: number;
|
||||||
|
added?: number;
|
||||||
total?: number;
|
total?: number;
|
||||||
}
|
}
|
||||||
| { id: number; ok: false; error: string };
|
| { id: number; ok: false; error: string };
|
||||||
|
|
||||||
let worker: Worker | null = null;
|
let worker: Worker | null = null;
|
||||||
let broken = false;
|
let broken = false;
|
||||||
|
let initialized = false;
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
const pending = new Map<
|
const pending = new Map<
|
||||||
number,
|
number,
|
||||||
@@ -84,18 +108,41 @@ function spawn(): Worker | null {
|
|||||||
|
|
||||||
function call(req: ReqWithoutId): Promise<Res> {
|
function call(req: ReqWithoutId): Promise<Res> {
|
||||||
if (!worker) return Promise.reject(new Error("no translation worker"));
|
if (!worker) return Promise.reject(new Error("no translation worker"));
|
||||||
|
const w = worker;
|
||||||
return new Promise<Res>((resolve, reject) => {
|
return new Promise<Res>((resolve, reject) => {
|
||||||
const id = nextId++;
|
const id = nextId++;
|
||||||
pending.set(id, { resolve, reject });
|
pending.set(id, { resolve, reject });
|
||||||
worker!.postMessage({ ...req, id } as Req);
|
w.postMessage({ ...req, id } as Req);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the master FurnitureData + each hotel's official translation map once and
|
||||||
|
* initialize the worker. Idempotent — safe to call before every operation.
|
||||||
|
* Reads happen on the main thread but only once per process, so the per-import
|
||||||
|
* CPU cost of parsing the 100k-entry master is amortized away.
|
||||||
|
*/
|
||||||
|
async function ensureInit(): Promise<void> {
|
||||||
|
if (!worker || initialized) return;
|
||||||
|
const { readFurniData } = await import("@/lib/services/furni-data");
|
||||||
|
const { fetchTranslationsForHotel, FURNIDATA_LANGUAGES } = await import(
|
||||||
|
"@/lib/services/furni-data-i18n"
|
||||||
|
);
|
||||||
|
const master = (await readFurniData()) as Record<string, unknown>;
|
||||||
|
const hotels = [...new Set(FURNIDATA_LANGUAGES.map((l) => l.hotel))];
|
||||||
|
const hotelMaps: Record<string, Map<string, Translation>> = {};
|
||||||
|
for (const h of hotels) hotelMaps[h] = await fetchTranslationsForHotel(h);
|
||||||
|
const res = await call({
|
||||||
|
type: "init",
|
||||||
|
masterJson: JSON.stringify(master),
|
||||||
|
hotelMaps,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(res.error);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TranslationWorker {
|
export interface TranslationWorker {
|
||||||
init(
|
ensureInit(): Promise<void>;
|
||||||
masterJson: string,
|
|
||||||
hotelMaps: Record<string, Map<string, Translation>>,
|
|
||||||
): Promise<void>;
|
|
||||||
prepare(lang: string, hotel: string): Promise<string[]>;
|
prepare(lang: string, hotel: string): Promise<string[]>;
|
||||||
finalize(
|
finalize(
|
||||||
lang: string,
|
lang: string,
|
||||||
@@ -106,6 +153,16 @@ export interface TranslationWorker {
|
|||||||
translatedWall: number;
|
translatedWall: number;
|
||||||
total: number;
|
total: number;
|
||||||
}>;
|
}>;
|
||||||
|
patchPrepare(
|
||||||
|
fileContent: string | null,
|
||||||
|
lang: string,
|
||||||
|
hotel: string,
|
||||||
|
entries: PatchEntry[],
|
||||||
|
): Promise<string[]>;
|
||||||
|
patchFinalize(
|
||||||
|
lang: string,
|
||||||
|
translations: Map<string, string>,
|
||||||
|
): Promise<{ json: string; patched: number; added: number; total: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTranslationWorker(): TranslationWorker | null {
|
export function getTranslationWorker(): TranslationWorker | null {
|
||||||
@@ -113,11 +170,11 @@ export function getTranslationWorker(): TranslationWorker | null {
|
|||||||
if (!worker) worker = spawn();
|
if (!worker) worker = spawn();
|
||||||
if (!worker) return null;
|
if (!worker) return null;
|
||||||
return {
|
return {
|
||||||
async init(masterJson, hotelMaps) {
|
async ensureInit() {
|
||||||
const res = await call({ type: "init", masterJson, hotelMaps });
|
await ensureInit();
|
||||||
if (!res.ok) throw new Error(res.error);
|
|
||||||
},
|
},
|
||||||
async prepare(lang, hotel) {
|
async prepare(lang, hotel) {
|
||||||
|
await ensureInit();
|
||||||
const res = await call({ type: "prepare", lang, hotel });
|
const res = await call({ type: "prepare", lang, hotel });
|
||||||
if (!res.ok || !res.pendingTexts)
|
if (!res.ok || !res.pendingTexts)
|
||||||
throw new Error(res.ok ? "missing pendingTexts" : res.error);
|
throw new Error(res.ok ? "missing pendingTexts" : res.error);
|
||||||
@@ -134,5 +191,29 @@ export function getTranslationWorker(): TranslationWorker | null {
|
|||||||
total: res.total ?? 0,
|
total: res.total ?? 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
async patchPrepare(fileContent, lang, hotel, entries) {
|
||||||
|
await ensureInit();
|
||||||
|
const res = await call({
|
||||||
|
type: "patchPrepare",
|
||||||
|
lang,
|
||||||
|
hotel,
|
||||||
|
fileContent,
|
||||||
|
entries,
|
||||||
|
});
|
||||||
|
if (!res.ok || !res.pendingTexts)
|
||||||
|
throw new Error(res.ok ? "missing pendingTexts" : res.error);
|
||||||
|
return res.pendingTexts;
|
||||||
|
},
|
||||||
|
async patchFinalize(lang, translations) {
|
||||||
|
const res = await call({ type: "patchFinalize", lang, translations });
|
||||||
|
if (!res.ok || res.json === undefined)
|
||||||
|
throw new Error(res.ok ? "missing json" : res.error);
|
||||||
|
return {
|
||||||
|
json: res.json,
|
||||||
|
patched: res.patched ?? 0,
|
||||||
|
added: res.added ?? 0,
|
||||||
|
total: res.total ?? 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,12 @@ import type { HabboGamedataHotel } from "@/lib/habbo-gamedata-hotel";
|
|||||||
import {
|
import {
|
||||||
type FurniDataMaster,
|
type FurniDataMaster,
|
||||||
finalizeLangFile,
|
finalizeLangFile,
|
||||||
|
finalizePatch,
|
||||||
|
type PatchEntry,
|
||||||
type PreparedLang,
|
type PreparedLang,
|
||||||
|
type PreparedPatch,
|
||||||
prepareLangFile,
|
prepareLangFile,
|
||||||
|
preparePatch,
|
||||||
type Translation,
|
type Translation,
|
||||||
} from "./furni-data-i18n-core";
|
} from "./furni-data-i18n-core";
|
||||||
|
|
||||||
@@ -26,19 +30,40 @@ interface FinalizeMsg {
|
|||||||
lang: string;
|
lang: string;
|
||||||
translations: Map<string, string>;
|
translations: Map<string, string>;
|
||||||
}
|
}
|
||||||
type Msg = InitMsg | PrepareMsg | FinalizeMsg;
|
interface PatchPrepareMsg {
|
||||||
|
id: number;
|
||||||
|
type: "patchPrepare";
|
||||||
|
lang: string;
|
||||||
|
hotel: string;
|
||||||
|
fileContent: string | null;
|
||||||
|
entries: PatchEntry[];
|
||||||
|
}
|
||||||
|
interface PatchFinalizeMsg {
|
||||||
|
id: number;
|
||||||
|
type: "patchFinalize";
|
||||||
|
lang: string;
|
||||||
|
translations: Map<string, string>;
|
||||||
|
}
|
||||||
|
type Msg =
|
||||||
|
| InitMsg
|
||||||
|
| PrepareMsg
|
||||||
|
| FinalizeMsg
|
||||||
|
| PatchPrepareMsg
|
||||||
|
| PatchFinalizeMsg;
|
||||||
|
|
||||||
let master: FurniDataMaster | null = null;
|
let master: FurniDataMaster | null = null;
|
||||||
let hotelMaps: Record<string, Map<string, Translation>> = {};
|
let hotelMaps: Record<string, Map<string, Translation>> = {};
|
||||||
const prepared = new Map<string, PreparedLang>();
|
const prepared = new Map<string, PreparedLang>();
|
||||||
|
const preparedPatches = new Map<string, PreparedPatch>();
|
||||||
|
|
||||||
if (parentPort) {
|
if (parentPort) {
|
||||||
parentPort.on("message", (msg: Msg) => {
|
const port = parentPort;
|
||||||
|
port.on("message", (msg: Msg) => {
|
||||||
try {
|
try {
|
||||||
if (msg.type === "init") {
|
if (msg.type === "init") {
|
||||||
master = JSON.parse(msg.masterJson);
|
master = JSON.parse(msg.masterJson);
|
||||||
hotelMaps = msg.hotelMaps ?? {};
|
hotelMaps = msg.hotelMaps ?? {};
|
||||||
parentPort!.postMessage({ id: msg.id, ok: true });
|
port.postMessage({ id: msg.id, ok: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.type === "prepare") {
|
if (msg.type === "prepare") {
|
||||||
@@ -52,21 +77,49 @@ if (parentPort) {
|
|||||||
officialMap,
|
officialMap,
|
||||||
);
|
);
|
||||||
prepared.set(msg.lang, r);
|
prepared.set(msg.lang, r);
|
||||||
parentPort!.postMessage({
|
port.postMessage({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
ok: true,
|
ok: true,
|
||||||
pendingTexts: r.pendingTexts,
|
pendingTexts: r.pendingTexts,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === "patchPrepare") {
|
||||||
|
if (!master) throw new Error("translation worker not initialized");
|
||||||
|
const officialMap =
|
||||||
|
hotelMaps[msg.hotel] ?? new Map<string, Translation>();
|
||||||
|
const r = preparePatch(
|
||||||
|
msg.fileContent,
|
||||||
|
master,
|
||||||
|
msg.lang,
|
||||||
|
msg.hotel as HabboGamedataHotel,
|
||||||
|
officialMap,
|
||||||
|
msg.entries,
|
||||||
|
);
|
||||||
|
preparedPatches.set(msg.lang, r);
|
||||||
|
port.postMessage({
|
||||||
|
id: msg.id,
|
||||||
|
ok: true,
|
||||||
|
pendingTexts: [...r.pendingTexts],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === "patchFinalize") {
|
||||||
|
const r = preparedPatches.get(msg.lang);
|
||||||
|
if (!r) throw new Error(`no prepared patch for ${msg.lang}`);
|
||||||
|
const res = finalizePatch(r, msg.translations);
|
||||||
|
preparedPatches.delete(msg.lang);
|
||||||
|
port.postMessage({ id: msg.id, ok: true, ...res });
|
||||||
|
return;
|
||||||
|
}
|
||||||
// finalize
|
// finalize
|
||||||
const r = prepared.get(msg.lang);
|
const r = prepared.get(msg.lang);
|
||||||
if (!r) throw new Error(`no prepared data for ${msg.lang}`);
|
if (!r) throw new Error(`no prepared data for ${msg.lang}`);
|
||||||
const res = finalizeLangFile(r.cloned, r.pendingRefs, msg.translations);
|
const res = finalizeLangFile(r.cloned, r.pendingRefs, msg.translations);
|
||||||
prepared.delete(msg.lang);
|
prepared.delete(msg.lang);
|
||||||
parentPort!.postMessage({ id: msg.id, ok: true, ...res });
|
port.postMessage({ id: msg.id, ok: true, ...res });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
parentPort!.postMessage({
|
port.postMessage({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
ok: false,
|
ok: false,
|
||||||
error: (err as Error).message,
|
error: (err as Error).message,
|
||||||
|
|||||||
Reference in new issue
Block a user