Files
openclaw/test/scripts/control-ui-i18n-sync-plan.test.ts
2026-07-12 15:09:23 +08:00

269 lines
7.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
createControlUiLocaleSyncPlan,
flattenTranslations,
resolveLocaleMetaProvenance,
type LocaleEntry,
type LocaleMeta,
type TranslationMemoryEntry,
} from "../../scripts/lib/control-ui-i18n-sync-plan.ts";
const entry: LocaleEntry = {
exportName: "fr",
fileName: "fr.ts",
languageKey: "fr",
locale: "fr",
};
const hashText = (text: string) => `hash:${text}`;
const cacheKeyFor = (key: string, textHash: string) => `cache:${key}:${textHash}`;
function memoryEntry(overrides: Partial<TranslationMemoryEntry> = {}): TranslationMemoryEntry {
return {
cache_key: "legacy-cache",
model: "legacy-model",
provider: "legacy-provider",
segment_id: "legacy.segment",
source_path: "ui/src/i18n/locales/fr.ts",
src_lang: "en",
text: "Shared",
text_hash: hashText("Shared"),
tgt_lang: "fr",
translated: "Partage",
updated_at: "2026-01-01T00:00:00.000Z",
...overrides,
};
}
function localeMeta(overrides: Partial<LocaleMeta> = {}): LocaleMeta {
return {
fallbackKeys: [],
generatedAt: "2026-01-01T00:00:00.000Z",
locale: "fr",
model: "legacy-model",
provider: "legacy-provider",
sourceHash: "old-source",
totalKeys: 0,
translatedKeys: 0,
workflow: 1,
...overrides,
};
}
describe("createControlUiLocaleSyncPlan", () => {
it("preserves provenance when a configured provider performs no translation", () => {
const previousMeta = localeMeta();
expect(
resolveLocaleMetaProvenance({
didTranslate: false,
model: "next-model",
previousMeta,
provider: "next-provider",
}),
).toEqual({ model: previousMeta.model, provider: previousMeta.provider });
expect(
resolveLocaleMetaProvenance({
didTranslate: true,
model: "next-model",
previousMeta,
provider: "next-provider",
}),
).toEqual({ model: "next-model", provider: "next-provider" });
});
it("plans reuse and renders deterministic locale artifacts", () => {
const sourceFlat = flattenTranslations({
group: {
cached: "Cached source",
existing: "Existing source",
pending: "Pending source",
reused: "Shared",
},
});
const exactCacheKey = cacheKeyFor("group.cached", hashText("Cached source"));
const exactCache = memoryEntry({
cache_key: exactCacheKey,
segment_id: "group.cached",
text: "Cached source",
text_hash: hashText("Cached source"),
translated: "En cache",
});
const sharedCache = memoryEntry();
const plan = createControlUiLocaleSyncPlan({
allowTranslate: false,
cacheKeyFor,
entry,
existingFlat: new Map([
["group.cached", "Ancien cache"],
["group.existing", "Existant"],
]),
force: false,
hashText,
previousMeta: localeMeta({ fallbackKeys: ["group.cached"] }),
sourceFlat,
sourceHash: "next-source",
translationMemory: new Map([
[sharedCache.cache_key, sharedCache],
[exactCache.cache_key, exactCache],
]),
});
expect(plan.pending.map((item) => item.key)).toEqual(["group.pending"]);
expect(plan.newFallbackCount).toBe(1);
const artifacts = plan.render({
defaultGlossary: [{ source: "OpenClaw", target: "OpenClaw" }],
generatedAt: "2026-02-02T00:00:00.000Z",
glossary: [],
model: "legacy-model",
provider: "legacy-provider",
workflow: 1,
});
expect(artifacts.localeModule).toBe(
[
"// Generated locale bundle for Control UI translations.",
"// Run `pnpm ui:i18n:sync` instead of editing this file directly.",
'import type { TranslationMap } from "../lib/types.ts";',
"",
"export const fr: TranslationMap = {",
" group: {",
' cached: "En cache",',
' existing: "Existant",',
' pending: "Pending source",',
' reused: "Partage",',
" },",
"};",
"",
].join("\n"),
);
expect(artifacts.meta).toBe(
`${JSON.stringify(
{
fallbackKeys: ["group.cached", "group.pending"],
generatedAt: "2026-02-02T00:00:00.000Z",
locale: "fr",
model: "legacy-model",
provider: "legacy-provider",
sourceHash: "next-source",
totalKeys: 4,
translatedKeys: 2,
workflow: 1,
},
null,
2,
)}\n`,
);
expect(artifacts.glossary).toBe(
`${JSON.stringify([{ source: "OpenClaw", target: "OpenClaw" }], null, 2)}\n`,
);
const clonedCache = {
...sharedCache,
cache_key: cacheKeyFor("group.reused", hashText("Shared")),
segment_id: "group.reused",
};
expect(artifacts.translationMemory).toBe(
`${[clonedCache, exactCache, sharedCache]
.toSorted((left, right) => left.cache_key.localeCompare(right.cache_key))
.map((value) => JSON.stringify(value))
.join("\n")}\n`,
);
});
it("refreshes recorded fallbacks and records translated replacements", () => {
const sourceFlat = flattenTranslations({ title: "New English" });
const previousMeta = localeMeta({
fallbackKeys: ["title"],
sourceHash: "previous-source",
totalKeys: 1,
translatedKeys: 0,
});
const plan = createControlUiLocaleSyncPlan({
allowTranslate: true,
cacheKeyFor,
entry,
existingFlat: new Map([["title", "Old English"]]),
force: true,
hashText,
previousMeta,
sourceFlat,
sourceHash: "next-source",
translationMemory: new Map(),
});
expect(plan.newFallbackCount).toBe(0);
plan.recordTranslations(plan.pending, new Map([["title", "Nouveau"]]), {
model: "next-model",
provider: "next-provider",
sourceLocale: "en",
updatedAt: () => "2026-02-02T00:00:00.000Z",
});
const artifacts = plan.render({
defaultGlossary: [],
generatedAt: "2026-03-03T00:00:00.000Z",
glossary: [],
model: "next-model",
provider: "next-provider",
workflow: 1,
});
expect(artifacts.fallbackCount).toBe(0);
expect(artifacts.nextFlat.get("title")).toBe("Nouveau");
expect(JSON.parse(artifacts.meta)).toMatchObject({
fallbackKeys: [],
generatedAt: "2026-03-03T00:00:00.000Z",
translatedKeys: 1,
});
expect(artifacts.translationMemory).toBe(
`${JSON.stringify(
memoryEntry({
cache_key: cacheKeyFor("title", hashText("New English")),
model: "next-model",
provider: "next-provider",
segment_id: "title",
text: "New English",
text_hash: hashText("New English"),
translated: "Nouveau",
updated_at: "2026-02-02T00:00:00.000Z",
}),
)}\n`,
);
});
it("preserves generatedAt when semantic metadata is unchanged", () => {
const sourceFlat = flattenTranslations({ title: "Titre" });
const previousMeta = localeMeta({
sourceHash: "same-source",
totalKeys: 1,
translatedKeys: 1,
});
const plan = createControlUiLocaleSyncPlan({
allowTranslate: false,
cacheKeyFor,
entry,
existingFlat: new Map([["title", "Titre"]]),
force: false,
hashText,
previousMeta,
sourceFlat,
sourceHash: "same-source",
translationMemory: new Map(),
});
const artifacts = plan.render({
defaultGlossary: [],
generatedAt: "2026-03-03T00:00:00.000Z",
glossary: [],
model: "legacy-model",
provider: "legacy-provider",
workflow: 1,
});
expect(JSON.parse(artifacts.meta)).toMatchObject({
generatedAt: previousMeta.generatedAt,
});
});
});