mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(ci): retry locale placeholder mismatches
This commit is contained in:
+28
-15
@@ -12,6 +12,7 @@ import {
|
||||
compareStringArrays,
|
||||
createControlUiLocaleSyncPlan,
|
||||
flattenTranslations,
|
||||
resolveLocaleMetaProvenance,
|
||||
type GlossaryEntry,
|
||||
type LocaleEntry,
|
||||
type LocaleMeta,
|
||||
@@ -1317,6 +1318,24 @@ function parseTranslationReply(raw: string): Record<string, unknown> {
|
||||
return JSON.parse(fenced ? fenced[1] : trimmed) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function parseTranslationBatchReply(
|
||||
raw: string,
|
||||
items: readonly TranslationBatchItem[],
|
||||
locale: string,
|
||||
): Map<string, string> {
|
||||
const parsed = parseTranslationReply(raw);
|
||||
const translated = new Map<string, string>();
|
||||
for (const item of items) {
|
||||
const value = parsed[item.key];
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`missing translation for ${item.key}`);
|
||||
}
|
||||
translated.set(item.key, value);
|
||||
}
|
||||
assertPlaceholderParity(new Map(items.map((item) => [item.key, item.text])), translated, locale);
|
||||
return translated;
|
||||
}
|
||||
|
||||
async function translateBatch(
|
||||
clientAccess: ClientAccess,
|
||||
items: readonly TranslationBatchItem[],
|
||||
@@ -1334,15 +1353,7 @@ async function translateBatch(
|
||||
const raw = await (
|
||||
await clientAccess.getClient()
|
||||
).prompt(buildBatchPrompt(items), attemptLabel);
|
||||
const parsed = parseTranslationReply(raw);
|
||||
const translated = new Map<string, string>();
|
||||
for (const item of items) {
|
||||
const value = parsed[item.key];
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new Error(`missing translation for ${item.key}`);
|
||||
}
|
||||
translated.set(item.key, value);
|
||||
}
|
||||
const translated = parseTranslationBatchReply(raw, items, context.locale);
|
||||
logProgress(`${attemptLabel}: done (${formatDuration(Date.now() - startedAt)})`);
|
||||
return translated;
|
||||
} catch (error) {
|
||||
@@ -1528,16 +1539,18 @@ async function syncLocale(
|
||||
// legitimately stay identical to English. Track fallback keys from actual
|
||||
// fallback decisions and previous fallback metadata instead.
|
||||
|
||||
const nextProvider = allowTranslate
|
||||
? resolveConfiguredProvider()
|
||||
: (previousMeta?.provider ?? "");
|
||||
const nextModel = allowTranslate ? resolveConfiguredModel() : (previousMeta?.model ?? "");
|
||||
const provenance = resolveLocaleMetaProvenance({
|
||||
didTranslate: allowTranslate && plan.pending.length > 0,
|
||||
model: allowTranslate ? resolveConfiguredModel() : "",
|
||||
previousMeta,
|
||||
provider: allowTranslate ? resolveConfiguredProvider() : "",
|
||||
});
|
||||
const artifacts = plan.render({
|
||||
defaultGlossary: DEFAULT_GLOSSARY,
|
||||
generatedAt: new Date().toISOString(),
|
||||
glossary,
|
||||
model: nextModel,
|
||||
provider: nextProvider,
|
||||
model: provenance.model,
|
||||
provider: provenance.provider,
|
||||
workflow: CONTROL_UI_I18N_WORKFLOW,
|
||||
});
|
||||
assertPlaceholderParity(sourceFlat, artifacts.nextFlat, entry.locale);
|
||||
|
||||
@@ -71,6 +71,21 @@ export function shouldReuseExistingTranslation(options: {
|
||||
return !options.isFallback || (!options.allowTranslate && !options.force);
|
||||
}
|
||||
|
||||
export function resolveLocaleMetaProvenance(options: {
|
||||
didTranslate: boolean;
|
||||
model: string;
|
||||
previousMeta: LocaleMeta | null;
|
||||
provider: string;
|
||||
}): { model: string; provider: string } {
|
||||
if (options.didTranslate) {
|
||||
return { model: options.model, provider: options.provider };
|
||||
}
|
||||
return {
|
||||
model: options.previousMeta?.model ?? options.model,
|
||||
provider: options.previousMeta?.provider ?? options.provider,
|
||||
};
|
||||
}
|
||||
|
||||
export function createControlUiLocaleSyncPlan(input: {
|
||||
allowTranslate: boolean;
|
||||
cacheKeyFor: (key: string, textHash: string) => string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createControlUiLocaleSyncPlan,
|
||||
flattenTranslations,
|
||||
resolveLocaleMetaProvenance,
|
||||
type LocaleEntry,
|
||||
type LocaleMeta,
|
||||
type TranslationMemoryEntry,
|
||||
@@ -50,6 +51,27 @@ function localeMeta(overrides: Partial<LocaleMeta> = {}): LocaleMeta {
|
||||
}
|
||||
|
||||
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: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendBoundedProcessOutput,
|
||||
parseTranslationBatchReply,
|
||||
runProcess,
|
||||
shouldReuseExistingTranslation,
|
||||
} from "../../scripts/control-ui-i18n.ts";
|
||||
@@ -50,6 +51,32 @@ async function waitForChildClose(
|
||||
}
|
||||
|
||||
describe("control-ui-i18n process runner", () => {
|
||||
it("rejects placeholder-corrupt batch replies before they leave the retry loop", () => {
|
||||
const items = [
|
||||
{
|
||||
cacheKey: "cache-key",
|
||||
key: "configView.viewPendingChange",
|
||||
text: "View pending change ({count})",
|
||||
textHash: "text-hash",
|
||||
},
|
||||
];
|
||||
|
||||
expect(() =>
|
||||
parseTranslationBatchReply(
|
||||
JSON.stringify({ "configView.viewPendingChange": "Pending change" }),
|
||||
items,
|
||||
"ar",
|
||||
),
|
||||
).toThrow("ar:configView.viewPendingChange expected {count} got {}");
|
||||
expect(
|
||||
parseTranslationBatchReply(
|
||||
JSON.stringify({ "configView.viewPendingChange": "Pending change ({count})" }),
|
||||
items,
|
||||
"ar",
|
||||
),
|
||||
).toEqual(new Map([["configView.viewPendingChange", "Pending change ({count})"]]));
|
||||
});
|
||||
|
||||
it("ships no recorded English fallbacks", () => {
|
||||
const metaDir = path.resolve("ui/src/i18n/.i18n");
|
||||
const fallbacks = readdirSync(metaDir)
|
||||
|
||||
Reference in New Issue
Block a user