import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { readFile, stat } from "node:fs/promises"; import { pathToFileURL } from "node:url"; import type { TranslationMap, TranslationMemoryEntry } from "./control-ui-i18n-sync-plan.ts"; async function importControlUiLocaleModule(filePath: string): Promise { const stats = await stat(filePath); return (await import(`${pathToFileURL(filePath).href}?ts=${stats.mtimeMs}`)) as T; } export async function loadControlUiLocaleCatalog( filePath: string, exportName: string, ): Promise { if (!existsSync(filePath)) { return null; } const module = await importControlUiLocaleModule>(filePath); return module[exportName] ?? null; } export async function loadControlUiSourceCatalog( sourceLocalePath: string, activitySourceLocalePath: string, sessionPlacementSourceLocalePath: string, ): Promise { const source = await loadControlUiLocaleCatalog(sourceLocalePath, "en"); const activitySource = ( await importControlUiLocaleModule<{ registerActivityEnglish: { catalog: TranslationMap }; }>(activitySourceLocalePath) ).registerActivityEnglish.catalog; const sessionPlacementSource = ( await importControlUiLocaleModule<{ registerSessionPlacementEnglish: { catalog: TranslationMap }; }>(sessionPlacementSourceLocalePath) ).registerSessionPlacementEnglish.catalog; if (!source || !activitySource || !sessionPlacementSource) { throw new Error("Control UI English source catalogs are incomplete"); } return mergeControlUiTranslationMaps(source, activitySource, sessionPlacementSource); } export async function readControlUiSourceCatalog( sourceLocalePath: string, activitySourceLocalePath: string, sessionPlacementSourceLocalePath: string, ): Promise { const sources = await Promise.all( [sourceLocalePath, activitySourceLocalePath, sessionPlacementSourceLocalePath].map((filePath) => readFile(filePath, "utf8"), ), ); return sources.join("\n"); } export function hashControlUiTranslationText(text: string): string { return createHash("sha256").update(text.trim().split(/\s+/).join(" ")).digest("hex"); } export function mergeControlUiTranslationMaps( ...maps: ReadonlyArray ): TranslationMap { const merged: TranslationMap = {}; const mergeInto = (target: TranslationMap, source: TranslationMap): void => { for (const [key, value] of Object.entries(source)) { if (typeof value === "string") { target[key] = value; continue; } const existing = target[key]; const nested = typeof existing === "object" ? existing : {}; target[key] = nested; mergeInto(nested, value); } }; for (const map of maps) { mergeInto(merged, map); } return merged; } export function loadControlUiTranslationMemory( filePath: string, ): Map { const entries = new Map(); if (!existsSync(filePath)) { return entries; } for (const line of readFileSync(filePath, "utf8").split("\n")) { if (!line.trim()) { continue; } const entry = JSON.parse(line) as TranslationMemoryEntry; if (entry.cache_key && entry.translated.trim()) { entries.set(entry.cache_key, entry); } } return entries; } function setControlUiCatalogValue(catalog: TranslationMap, key: string, value: string): void { const parts = key.split("."); let current = catalog; for (const part of parts.slice(0, -1)) { const existing = current[part]; if (!existing || typeof existing === "string") { current[part] = {}; } current = current[part] as TranslationMap; } current[parts[parts.length - 1]!] = value; } export function materializeControlUiLocaleCatalog( sourceFlat: ReadonlyMap, memory: ReadonlyMap, ): TranslationMap { const translations = new Map(); for (const entry of memory.values()) { for (const key of [entry.segment_id, ...(entry.segment_ids ?? [])]) { const source = sourceFlat.get(key); if (source === undefined || entry.text_hash !== hashControlUiTranslationText(source)) { continue; } translations.set(key, entry.translated); } } const catalog: TranslationMap = {}; for (const key of sourceFlat.keys()) { const translated = translations.get(key); if (translated !== undefined) { setControlUiCatalogValue(catalog, key, translated); } } return catalog; }