fix(whats-new): support localized Arena CTA labels (#7943)

Co-authored-by: zhouzheng <zhouzheng@refly.ai>
This commit is contained in:
open-design-crew[bot]
2026-09-09 09:36:40 +00:00
committed by GitHub
co-authored by zhouzheng
parent f197844e27
commit 21bba485b6
11 changed files with 104 additions and 10 deletions
+4
View File
@@ -82,9 +82,11 @@ function parseLocaleOverrides(value: unknown): Record<string, WhatsNewLocaleCont
const title = readNonEmptyString(entry.title);
const body = readNonEmptyString(entry.body);
const linkUrl = readHttpsUrl(entry.linkUrl);
const ctaLabel = readNonEmptyString(entry.ctaLabel);
if (title != null) override.title = title;
if (body != null) override.body = body;
if (linkUrl != null) override.linkUrl = linkUrl;
if (ctaLabel != null) override.ctaLabel = ctaLabel;
if (Object.keys(override).length > 0) locales[locale] = override;
}
return Object.keys(locales).length > 0 ? locales : undefined;
@@ -105,6 +107,7 @@ export function parseWhatsNewDocument(payload: unknown): { id: string | null; co
const imageUrl = readHttpsUrl(payload.imageUrl);
const linkUrl = readHttpsUrl(payload.linkUrl);
const ctaLabel = readNonEmptyString(payload.ctaLabel);
const locales = parseLocaleOverrides(payload.locales);
return {
id,
@@ -113,6 +116,7 @@ export function parseWhatsNewDocument(payload: unknown): { id: string | null; co
body,
...(imageUrl != null ? { imageUrl } : {}),
...(linkUrl != null ? { linkUrl } : {}),
...(ctaLabel != null ? { ctaLabel } : {}),
...(locales != null ? { locales } : {}),
},
};
+21
View File
@@ -50,6 +50,27 @@ describe('parseWhatsNewDocument', () => {
expect(content?.locales?.['zh-CN']?.title).toBe('设计系统同步');
});
it('preserves and trims configured CTA labels through the document parser', () => {
const { content } = parseWhatsNewDocument({
...DOC,
ctaLabel: ' View Arena ',
locales: { 'zh-CN': { ctaLabel: ' 查看评测站 ' } },
});
expect(content).toMatchObject({
ctaLabel: 'View Arena',
locales: { 'zh-CN': { ctaLabel: '查看评测站' } },
});
});
it.each(['', ' ', 42, null])('drops an invalid optional CTA label (%j) without hiding content', (ctaLabel) => {
const { content } = parseWhatsNewDocument({
...DOC, ctaLabel, locales: { 'zh-CN': { title: '设计系统同步', ctaLabel } },
});
expect(content?.title).toBe(DOC.title);
expect(content).not.toHaveProperty('ctaLabel');
expect(content?.locales?.['zh-CN']).toEqual({ title: '设计系统同步' });
});
it('rejects a document without an id (no show-once key)', () => {
const { id, content } = parseWhatsNewDocument({ ...DOC, id: '' });
expect(id).toBeNull();
+4 -2
View File
@@ -26,7 +26,7 @@ import styles from './WhatsNewPopup.module.css';
// `id` (see ../lib/whats-new).
//
// It reports what already happened, so its only actions are "close" and "open
// the release notes". Applying an update belongs to the real updater indicator
// the highlight link". Applying an update belongs to the real updater indicator
// (see ./UpdaterPopup.tsx) — this surface must never claim to install anything.
// Fallback for the CTA when the highlight document omits an explicit link.
@@ -65,6 +65,7 @@ type CardModel = {
notes: string[];
imageUrl: string | null;
linkUrl: string;
ctaLabel: string | null;
};
// `active` reports whether Home is the active entry view. EntryShell keeps
@@ -103,6 +104,7 @@ export function WhatsNewPopup({ active }: { active: boolean }) {
notes: whatsNewNotesFromBody(localized.body),
imageUrl: info.content.imageUrl ?? null,
linkUrl: localized.linkUrl ?? RELEASES_INDEX_URL,
ctaLabel: localized.ctaLabel,
});
});
return () => {
@@ -195,7 +197,7 @@ export function WhatsNewPopup({ active }: { active: boolean }) {
variant="primary"
onClick={openLink}
>
{t('whatsNew.cta')}
{card.ctaLabel ?? t('whatsNew.cta')}
</Button>
</div>
</Dialog>
+2 -1
View File
@@ -68,11 +68,12 @@ export function whatsNewNotesFromBody(body: string): string[] {
export function localizedWhatsNewContent(
content: WhatsNewContent,
locale: string,
): { title: string; body: string; linkUrl: string | null } {
): { title: string; body: string; linkUrl: string | null; ctaLabel: string | null } {
const override = content.locales?.[locale] ?? content.locales?.[locale.split('-')[0] ?? ''] ?? null;
return {
title: override?.title ?? content.title,
body: override?.body ?? content.body,
linkUrl: override?.linkUrl ?? content.linkUrl ?? null,
ctaLabel: override?.ctaLabel ?? content.ctaLabel ?? null,
};
}
@@ -320,6 +320,31 @@ describe('WhatsNewPopup actions', () => {
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
});
it.each([
['en', 'View Arena', 'https://open-design.ai/llm-arena-for-design/'],
['zh-CN', '查看评测站', 'https://open-design.ai/zh/llm-arena-for-design/'],
])('uses the configured %s CTA and matching Arena destination', async (locale, label, url) => {
window.localStorage.setItem('open-design:locale', locale!);
window.localStorage.setItem('open-design:locale-source', 'manual');
mockedFetchWhatsNew.mockResolvedValue({
...SHOW_PAYLOAD,
content: {
...SHOW_PAYLOAD.content!,
ctaLabel: 'View Arena',
linkUrl: 'https://open-design.ai/llm-arena-for-design/',
locales: {
'zh-CN': { ctaLabel: '查看评测站', linkUrl: 'https://open-design.ai/zh/llm-arena-for-design/' },
},
},
});
renderCard(true);
const cta = await screen.findByRole('button', { name: label });
fireEvent.click(cta);
expect(mockedOpenExternalUrl).toHaveBeenCalledWith(url);
expect(window.localStorage.getItem(WHATS_NEW_LAST_SEEN_STORAGE_KEY)).toBe(SHOW_PAYLOAD.id);
expect(screen.queryByTestId('whats-new-popup')).toBeNull();
});
it('close marks the highlight seen and opens no link', async () => {
renderCard(true);
+12
View File
@@ -67,6 +67,18 @@ describe('localizedWhatsNewContent', () => {
expect(zh.linkUrl).toBe('https://example.com/blog');
});
it('resolves CTA labels through the existing locale and base fallback', () => {
const content = {
...CONTENT,
ctaLabel: 'View Arena',
locales: { zh: { ctaLabel: '查看评测站' }, 'zh-CN': { ctaLabel: '查看评测站(中文)' } },
};
expect(localizedWhatsNewContent(content, 'zh-CN')).toHaveProperty('ctaLabel', '查看评测站(中文)');
expect(localizedWhatsNewContent(content, 'zh-TW')).toHaveProperty('ctaLabel', '查看评测站');
expect(localizedWhatsNewContent(content, 'fr')).toHaveProperty('ctaLabel', 'View Arena');
expect(localizedWhatsNewContent(CONTENT, 'en')).toHaveProperty('ctaLabel', null);
});
it('falls back to the base copy for unknown locales', () => {
const fr = localizedWhatsNewContent(CONTENT, 'fr');
expect(fr.title).toBe(CONTENT.title);
+3 -1
View File
@@ -4,11 +4,13 @@
"body": "Over a dozen models tested: Compare design quality and cost across models evaluated with the OpenDesign Harness.\nDeepSeek V4.1 Flash Leads in Scores: DeepSeek V4.1 Flash achieved 98% of top-ranked GPT-6 Astra’s average score at just 1% of its average cost.\nFind the right model for you: Explore the benchmarks to choose a model for your design needs and budget.",
"imageUrl": "https://whatsnew.open-design.ai/0.18.0.webp",
"linkUrl": "https://open-design.ai/llm-arena-for-design/",
"ctaLabel": "View Arena",
"locales": {
"zh-CN": {
"title": "OpenDesign Arena 正式上线",
"body": "十多款模型实测:使用 OpenDesign Harness,对比模型的设计表现与成本。\nDeepSeek V4.1 Flash 评分领先:DeepSeek V4.1 Flash 的均分达到榜首 GPT-6 Astra 的 98%,平均成本仅为其 1%。\n找到适合你的模型:前往 Arena,根据创作需求与预算选择模型。",
"linkUrl": "https://open-design.ai/zh/llm-arena-for-design/"
"linkUrl": "https://open-design.ai/zh/llm-arena-for-design/",
"ctaLabel": "查看评测站"
}
}
}
+7 -2
View File
@@ -64,11 +64,13 @@ down is an explicit act rather than something a typo can do for you.
"body": "Import, edit and sync design systems with cleaner release highlights on Home.",
"imageUrl": "https://whatsnew.open-design.ai/0.13.0.png",
"linkUrl": "https://github.com/nexu-io/open-design/releases/tag/open-design-v0.13.0",
"ctaLabel": "View release notes",
"locales": {
"zh-CN": {
"title": "设计系统同步",
"body": "在首页导入、编辑并同步设计系统,发布亮点更清晰。",
"linkUrl": "https://open-design.ai/zh/blog/0-13-0/"
"linkUrl": "https://open-design.ai/zh/blog/0-13-0/",
"ctaLabel": "查看更新说明"
}
}
}
@@ -83,8 +85,11 @@ parser rather than trusting review:
- `imageUrl` — optional, must be `https:`. Omitted → text-only card.
- `linkUrl` — optional, must be `https:`. Omitted → the CTA falls back to the
GitHub releases index.
- `ctaLabel` — optional, non-empty plain-text button label. Omitted → the
client's localized release-notes label. Clients predating this field ignore
it and keep their built-in label; they need a client update to support it.
- `locales` — optional per-locale overrides keyed by app locale id (`en`,
`zh-CN`, …); each may override `title`/`body`/`linkUrl`. An exact locale wins,
`zh-CN`, …); each may override `title`/`body`/`linkUrl`/`ctaLabel`. An exact locale wins,
then the bare language (`zh` for `zh-TW`), then the base fields.
## Updating the card
@@ -68,6 +68,20 @@ describe("what's new document guard", () => {
await expect(check(validDocument)).resolves.toBe(true);
});
test("accepts configurable base and localized CTA labels", async () => {
await expect(check({
...validDocument, ctaLabel: "View Arena",
locales: { "zh-CN": { ctaLabel: "查看评测站" } },
})).resolves.toBe(true);
});
test.each(["", " ", 42, null])("rejects invalid CTA labels (%j) before publishing", async (ctaLabel) => {
await expect(check({ ...validDocument, ctaLabel })).resolves.toBe(false);
await expect(check({
...validDocument, locales: { "zh-CN": { title: "标题", ctaLabel } },
})).resolves.toBe(false);
});
test("the empty retirement document passes", async () => {
await expect(check({})).resolves.toBe(true);
});
+4
View File
@@ -10,6 +10,8 @@ export interface WhatsNewLocaleContent {
title?: string;
body?: string;
linkUrl?: string;
/** Optional plain-text action label; omitted uses the client's localized default. */
ctaLabel?: string;
}
export interface WhatsNewContent {
@@ -21,6 +23,8 @@ export interface WhatsNewContent {
imageUrl?: string;
/** HTTPS link the "view the release notes" action opens. */
linkUrl?: string;
/** Optional plain-text action label; omitted uses the client's localized default. */
ctaLabel?: string;
locales?: Record<string, WhatsNewLocaleContent>;
}
+8 -4
View File
@@ -43,8 +43,8 @@ export const WHATS_NEW_DOCUMENT_PATH = "docs/whats-new.json";
// typo (`imageURL`, `link_url`, `locale`) that the parser would drop without
// complaint, which is exactly the silent-no-card failure this guard exists
// to catch.
const KNOWN_TOP_LEVEL_KEYS = new Set(["id", "title", "body", "imageUrl", "linkUrl", "locales"]);
const KNOWN_LOCALE_KEYS = new Set(["title", "body", "linkUrl"]);
const KNOWN_TOP_LEVEL_KEYS = new Set(["id", "title", "body", "imageUrl", "linkUrl", "ctaLabel", "locales"]);
const KNOWN_LOCALE_KEYS = new Set(["title", "body", "linkUrl", "ctaLabel"]);
function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -112,6 +112,10 @@ export async function checkWhatsNewDocument(root: string = repoRoot): Promise<bo
violations.push("`linkUrl` is present but the parser dropped it; it must be a non-empty https: URL");
}
if (payload.ctaLabel !== undefined && content.ctaLabel == null) {
violations.push("`ctaLabel` is present but the parser dropped it; it must be a non-empty string");
}
const rawLocales = payload.locales;
if (rawLocales !== undefined) {
if (!isObject(rawLocales)) {
@@ -119,7 +123,7 @@ export async function checkWhatsNewDocument(root: string = repoRoot): Promise<bo
} else {
for (const [locale, entry] of Object.entries(rawLocales)) {
if (!isObject(entry)) {
violations.push(`locale ${JSON.stringify(locale)} must be an object of title/body/linkUrl overrides`);
violations.push(`locale ${JSON.stringify(locale)} must be an object of title/body/linkUrl/ctaLabel overrides`);
continue;
}
for (const key of Object.keys(entry)) {
@@ -134,7 +138,7 @@ export async function checkWhatsNewDocument(root: string = repoRoot): Promise<bo
}
for (const key of Object.keys(entry)) {
if (!KNOWN_LOCALE_KEYS.has(key)) continue;
if (overrides[key as "title" | "body" | "linkUrl"] == null) {
if (overrides[key as "title" | "body" | "linkUrl" | "ctaLabel"] == null) {
violations.push(
`locale ${JSON.stringify(locale)} declares ${JSON.stringify(key)} but the parser dropped it; strings must be non-empty and linkUrl must be https:`,
);