mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
bbbd70542b
* feat(sessions): recover offline device placements * chore(protocol): refresh session placement models * perf(ui): lazy-load session placement recovery * test(ui): remove dropdown timing assertion * test(ui): await committed cloud recovery route * fix(sessions): complete explicit abandonment locally * fix(sessions): fence lists by runner availability * fix(ui): preserve canonical session freshness * fix(sessions): preserve recovery contracts after rebase * test(ui): await durable cloud recovery entry * test(gateway): complete current runner fixtures * fix(gateway): publish runner availability edges * fix(ui): preserve shared session freshness * fix(ui): preserve canonical sidebar session state * fix(sessions): preserve abandoned partials and run-owned replies * fix(sessions): resume durable abandonment retries * test(gateway): compose provisioning replay with runner availability * fix(sessions): publish recovered move transitions
141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
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<T>(filePath: string): Promise<T> {
|
|
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<TranslationMap | null> {
|
|
if (!existsSync(filePath)) {
|
|
return null;
|
|
}
|
|
const module = await importControlUiLocaleModule<Record<string, TranslationMap>>(filePath);
|
|
return module[exportName] ?? null;
|
|
}
|
|
|
|
export async function loadControlUiSourceCatalog(
|
|
sourceLocalePath: string,
|
|
activitySourceLocalePath: string,
|
|
sessionPlacementSourceLocalePath: string,
|
|
): Promise<TranslationMap> {
|
|
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<string> {
|
|
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>
|
|
): 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<string, TranslationMemoryEntry> {
|
|
const entries = new Map<string, TranslationMemoryEntry>();
|
|
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<string, string>,
|
|
memory: ReadonlyMap<string, TranslationMemoryEntry>,
|
|
): TranslationMap {
|
|
const translations = new Map<string, string>();
|
|
|
|
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;
|
|
}
|