From b378319d4aaedfa9fe7f8ca29faae14f887e52e8 Mon Sep 17 00:00:00 2001
From: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
Date: Fri, 14 Aug 2026 07:22:07 +0900
Subject: [PATCH] fix(webui): enforce localized group copy
---
webui/src/App.tsx | 2 --
webui/src/components/ChatList.tsx | 27 ++++++-----------------
webui/src/i18n/locales/en/common.json | 2 +-
webui/src/i18n/locales/es/common.json | 2 +-
webui/src/i18n/locales/fr/common.json | 2 +-
webui/src/i18n/locales/id/common.json | 2 +-
webui/src/i18n/locales/ja/common.json | 2 +-
webui/src/i18n/locales/ko/common.json | 2 +-
webui/src/i18n/locales/pt-BR/common.json | 2 +-
webui/src/i18n/locales/vi/common.json | 2 +-
webui/src/i18n/locales/zh-CN/common.json | 2 +-
webui/src/i18n/locales/zh-TW/common.json | 2 +-
webui/src/tests/app-layout.test.tsx | 10 ++++-----
webui/src/tests/chat-list.test.tsx | 28 ++++++++++++------------
webui/src/tests/i18n.test.tsx | 28 ++++++++++++++++++++++++
15 files changed, 64 insertions(+), 51 deletions(-)
diff --git a/webui/src/App.tsx b/webui/src/App.tsx
index 43729e05b..c7a5e942d 100644
--- a/webui/src/App.tsx
+++ b/webui/src/App.tsx
@@ -2718,7 +2718,6 @@ function Shell({
addPaneDisabled={creatingPane || activePaneLimitReached}
addPaneDisabledLabel={activePaneLimitReached
? t("workbench.paneLimit", {
- defaultValue: "Maximum {{count}} panes",
count: MAX_WORKBENCH_PANES,
})
: undefined}
@@ -2812,7 +2811,6 @@ function Shell({
composerPortalTarget={context.composerPortalTarget}
composerActive={context.active}
composerInputAriaLabel={t("workbench.composerAria", {
- defaultValue: "Message {{title}}",
title: pane.title,
})}
emptyComposerVariant="thread"
diff --git a/webui/src/components/ChatList.tsx b/webui/src/components/ChatList.tsx
index 2d178244e..e8d4735ae 100644
--- a/webui/src/components/ChatList.tsx
+++ b/webui/src/components/ChatList.tsx
@@ -1013,9 +1013,7 @@ export const ChatList = memo(function ChatList({
{paneGroup && onCreateTab ? (
onCreateTab(s.key)}>
- {t("workbench.createGroup", {
- defaultValue: "Create group",
- })}
+ {t("workbench.createGroup")}
) : null}
{paneGroup && onAttachPane ? (
@@ -1220,7 +1218,7 @@ function WorkbenchTabHeader({
{onDissolve ? (
- {t("workbench.dissolveTab", { defaultValue: "Dissolve group" })}
+ {t("workbench.dissolveTab")}
) : null}
- {t("workbench.deleteConversations", {
- defaultValue: "Delete all chats",
- })}
+ {t("workbench.deleteConversations")}
@@ -1322,10 +1318,7 @@ function ActivePaneRows({
return (
{panes.map((pane) => {
@@ -1335,10 +1328,7 @@ function ActivePaneRows({
: updated.has(pane.chatId) && !active
? "updated"
: null;
- const paneActionsLabel = t("workbench.paneActions", {
- defaultValue: "{{title}} pane actions",
- title: pane.title,
- });
+ const paneActionsLabel = t("workbench.paneActions", { title: pane.title });
const selected = selectedDeleteKeys.has(pane.key);
const isPinned = pinned.has(pane.key);
const isArchived = archived.has(pane.key);
@@ -1452,10 +1442,7 @@ function ActivePaneRows({
{onDetachPane ? (
onDetachPane(group.tabKey, pane.key)}>
- {t("workbench.detachPane", {
- defaultValue: "Remove",
- title: pane.title,
- })}
+ {t("workbench.detachPane", { title: pane.title })}
) : null}
{onAttachPane ? (
@@ -1521,7 +1508,7 @@ function MoveToGroupSubmenu({
- {t("workbench.moveTo", { defaultValue: "Move to" })}
+ {t("workbench.moveTo")}
{targets.map((target) => (
diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json
index b108a01e6..a90a2f0b4 100644
--- a/webui/src/i18n/locales/en/common.json
+++ b/webui/src/i18n/locales/en/common.json
@@ -1451,7 +1451,7 @@
"workbench": {
"aria": "Conversation workbench",
"panes": "Panes",
- "tabAria": "Tab: {{title}}",
+ "tabAria": "Group: {{title}}",
"panesInTab": "Panes in {{title}}",
"collapseTabGroup": "Collapse panes in {{title}}",
"expandTabGroup": "Expand panes in {{title}}",
diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json
index fd8ab197e..87932c357 100644
--- a/webui/src/i18n/locales/es/common.json
+++ b/webui/src/i18n/locales/es/common.json
@@ -1438,7 +1438,7 @@
"workbench": {
"aria": "Área de conversaciones",
"panes": "Paneles",
- "tabAria": "Pestaña: {{title}}",
+ "tabAria": "Grupo: {{title}}",
"panesInTab": "Paneles de {{title}}",
"collapseTabGroup": "Contraer los paneles de {{title}}",
"expandTabGroup": "Expandir los paneles de {{title}}",
diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json
index 822397e50..d60356ffa 100644
--- a/webui/src/i18n/locales/fr/common.json
+++ b/webui/src/i18n/locales/fr/common.json
@@ -1437,7 +1437,7 @@
"workbench": {
"aria": "Espace de conversations",
"panes": "Volets",
- "tabAria": "Onglet : {{title}}",
+ "tabAria": "Groupe : {{title}}",
"panesInTab": "Volets dans {{title}}",
"collapseTabGroup": "Réduire les volets de {{title}}",
"expandTabGroup": "Développer les volets de {{title}}",
diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json
index f25a2767f..b2d73b44b 100644
--- a/webui/src/i18n/locales/id/common.json
+++ b/webui/src/i18n/locales/id/common.json
@@ -1437,7 +1437,7 @@
"workbench": {
"aria": "Ruang kerja percakapan",
"panes": "Panel",
- "tabAria": "Tab: {{title}}",
+ "tabAria": "Grup: {{title}}",
"panesInTab": "Panel di {{title}}",
"collapseTabGroup": "Ciutkan panel di {{title}}",
"expandTabGroup": "Luaskan panel di {{title}}",
diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json
index 0b67d35cb..851860d4e 100644
--- a/webui/src/i18n/locales/ja/common.json
+++ b/webui/src/i18n/locales/ja/common.json
@@ -1437,7 +1437,7 @@
"workbench": {
"aria": "会話ワークベンチ",
"panes": "ペイン",
- "tabAria": "タブ:{{title}}",
+ "tabAria": "グループ:{{title}}",
"panesInTab": "{{title}} のペイン",
"collapseTabGroup": "{{title}} のペインを折りたたむ",
"expandTabGroup": "{{title}} のペインを展開する",
diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json
index 88a56346c..7d17a7149 100644
--- a/webui/src/i18n/locales/ko/common.json
+++ b/webui/src/i18n/locales/ko/common.json
@@ -1437,7 +1437,7 @@
"workbench": {
"aria": "대화 워크벤치",
"panes": "창",
- "tabAria": "탭: {{title}}",
+ "tabAria": "그룹: {{title}}",
"panesInTab": "{{title}}의 창",
"collapseTabGroup": "{{title}}의 창 접기",
"expandTabGroup": "{{title}}의 창 펼치기",
diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json
index 9e6bbd0a1..3310e1c0b 100644
--- a/webui/src/i18n/locales/pt-BR/common.json
+++ b/webui/src/i18n/locales/pt-BR/common.json
@@ -1451,7 +1451,7 @@
"workbench": {
"aria": "Área de conversas",
"panes": "Painéis",
- "tabAria": "Aba: {{title}}",
+ "tabAria": "Grupo: {{title}}",
"panesInTab": "Painéis em {{title}}",
"collapseTabGroup": "Recolher os painéis em {{title}}",
"expandTabGroup": "Expandir os painéis em {{title}}",
diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json
index 3bad5d74d..3025ac67a 100644
--- a/webui/src/i18n/locales/vi/common.json
+++ b/webui/src/i18n/locales/vi/common.json
@@ -1437,7 +1437,7 @@
"workbench": {
"aria": "Không gian hội thoại",
"panes": "Khung",
- "tabAria": "Thẻ: {{title}}",
+ "tabAria": "Nhóm: {{title}}",
"panesInTab": "Các khung trong {{title}}",
"collapseTabGroup": "Thu gọn các khung trong {{title}}",
"expandTabGroup": "Mở rộng các khung trong {{title}}",
diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json
index 07c90d081..709b42ccd 100644
--- a/webui/src/i18n/locales/zh-CN/common.json
+++ b/webui/src/i18n/locales/zh-CN/common.json
@@ -1451,7 +1451,7 @@
"workbench": {
"aria": "会话工作台",
"panes": "窗格",
- "tabAria": "标签页:{{title}}",
+ "tabAria": "分组:{{title}}",
"panesInTab": "{{title}} 中的窗格",
"collapseTabGroup": "折叠 {{title}} 中的窗格",
"expandTabGroup": "展开 {{title}} 中的窗格",
diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json
index eef3d8401..9e3763d85 100644
--- a/webui/src/i18n/locales/zh-TW/common.json
+++ b/webui/src/i18n/locales/zh-TW/common.json
@@ -1437,7 +1437,7 @@
"workbench": {
"aria": "對話工作台",
"panes": "窗格",
- "tabAria": "標籤頁:{{title}}",
+ "tabAria": "群組:{{title}}",
"panesInTab": "{{title}} 中的窗格",
"collapseTabGroup": "收合 {{title}} 中的窗格",
"expandTabGroup": "展開 {{title}} 中的窗格",
diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx
index adb863655..0bba2d149 100644
--- a/webui/src/tests/app-layout.test.tsx
+++ b/webui/src/tests/app-layout.test.tsx
@@ -3099,7 +3099,7 @@ describe("App layout", () => {
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
- const alphaTab = await within(sidebar).findByRole("button", { name: "Tab: Alpha tab" });
+ const alphaTab = await within(sidebar).findByRole("button", { name: "Group: Alpha tab" });
const betaTab = within(sidebar).getByRole("button", { name: "Beta tab" });
expect(alphaTab.compareDocumentPosition(betaTab) & Node.DOCUMENT_POSITION_FOLLOWING)
.toBeTruthy();
@@ -3218,7 +3218,7 @@ describe("App layout", () => {
statusHandlers.forEach((handler) => handler("open"));
});
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
- expect(within(sidebar).queryByRole("button", { name: "Tab: Solo pane" }))
+ expect(within(sidebar).queryByRole("button", { name: "Group: Solo pane" }))
.not.toBeInTheDocument();
setSidebarStateSpy.mockClear();
@@ -3228,14 +3228,14 @@ describe("App layout", () => {
fireEvent.click(await screen.findByRole("menuitem", { name: "Create group" }));
const tabButton = await within(sidebar).findByRole("button", {
- name: "Tab: Solo pane",
+ name: "Group: Solo pane",
});
const tabGroup = tabButton.closest("[data-sidebar-tab-group]") as HTMLElement;
expect(within(tabGroup).getByRole("list", { name: "Panes in Solo pane" }))
.toBeInTheDocument();
expect(within(tabGroup).getAllByRole("button", { name: "Solo pane" }))
.toHaveLength(1);
- expect(within(sidebar).queryByRole("button", { name: "Tab: Other pane" }))
+ expect(within(sidebar).queryByRole("button", { name: "Group: Other pane" }))
.not.toBeInTheDocument();
await waitFor(() => expect(setSidebarStateSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -3312,7 +3312,7 @@ describe("App layout", () => {
render();
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
const secondSidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
- expect(await within(secondSidebar).findByRole("button", { name: "Tab: Solo pane" }))
+ expect(await within(secondSidebar).findByRole("button", { name: "Group: Solo pane" }))
.toBeInTheDocument();
});
diff --git a/webui/src/tests/chat-list.test.tsx b/webui/src/tests/chat-list.test.tsx
index e19afae86..54f561eef 100644
--- a/webui/src/tests/chat-list.test.tsx
+++ b/webui/src/tests/chat-list.test.tsx
@@ -90,7 +90,7 @@ describe("ChatList", () => {
/>,
);
- expect(screen.getByRole("button", { name: "Tab: Root topic" }))
+ expect(screen.getByRole("button", { name: "Group: Root topic" }))
.toHaveAttribute("draggable", "false");
const pane = screen.getByRole("button", { name: "Research pane" });
expect(pane).toHaveAttribute("draggable", "true");
@@ -209,7 +209,7 @@ describe("ChatList", () => {
/>,
);
- expect(screen.queryByRole("button", { name: "Tab: Solo pane" }))
+ expect(screen.queryByRole("button", { name: "Group: Solo pane" }))
.not.toBeInTheDocument();
expect(screen.getAllByText("Solo pane")).toHaveLength(1);
expect(screen.queryByRole("list", { name: "Panes in Solo pane" }))
@@ -293,10 +293,10 @@ describe("ChatList", () => {
types: [SESSION_DRAG_TYPE],
} as unknown as DataTransfer;
const source = screen.getByRole("button", { name: "Solo topic" });
- const targetGroup = screen.getByRole("button", { name: "Tab: Target group" })
+ const targetGroup = screen.getByRole("button", { name: "Group: Target group" })
.closest("[data-sidebar-tab-group]")!;
const targetSurface = targetGroup.querySelector("[data-workbench-tab-surface]")!;
- const fullGroup = screen.getByRole("button", { name: "Tab: Full group" })
+ const fullGroup = screen.getByRole("button", { name: "Group: Full group" })
.closest("[data-sidebar-tab-group]")!;
const fullSurface = fullGroup.querySelector("[data-workbench-tab-surface]")!;
@@ -367,7 +367,7 @@ describe("ChatList", () => {
types: [SESSION_DRAG_TYPE],
} as unknown as DataTransfer;
const source = screen.getByRole("button", { name: "Research pane" });
- const sourceSurface = screen.getByRole("button", { name: "Tab: Root topic" })
+ const sourceSurface = screen.getByRole("button", { name: "Group: Root topic" })
.closest("[data-workbench-tab-surface]")!;
const standaloneList = document.querySelector("[data-chat-list-content]")!;
@@ -443,7 +443,7 @@ describe("ChatList", () => {
expect(child.closest("[data-sidebar-pane]"))
.toHaveAttribute("data-sidebar-pane", "websocket:child");
expect(child).toHaveAttribute("aria-current", "true");
- const targetTabRow = screen.getByRole("button", { name: "Tab: Target tab" })
+ const targetTabRow = screen.getByRole("button", { name: "Group: Target tab" })
.closest("li")!;
const targetChild = within(targetTabRow).getByRole("button", {
name: "Target research",
@@ -464,7 +464,7 @@ describe("ChatList", () => {
expect(onSelect).not.toHaveBeenCalled();
onSelectPane.mockClear();
- const rootTab = screen.getByRole("button", { name: "Tab: Root topic" });
+ const rootTab = screen.getByRole("button", { name: "Group: Root topic" });
fireEvent.click(rootTab);
expect(onSelectPane).not.toHaveBeenCalled();
expect(onSelect).not.toHaveBeenCalled();
@@ -510,7 +510,7 @@ describe("ChatList", () => {
fireEvent.keyDown(document, { key: "Escape" });
expect(child).toHaveAttribute("draggable", "false");
- expect(screen.getByRole("button", { name: "Tab: Target tab" }))
+ expect(screen.getByRole("button", { name: "Group: Target tab" }))
.toHaveAttribute("draggable", "false");
});
@@ -547,7 +547,7 @@ describe("ChatList", () => {
/>,
);
- const tabButton = screen.getByRole("button", { name: "Tab: Root topic" });
+ const tabButton = screen.getByRole("button", { name: "Group: Root topic" });
const tabGroup = tabButton.closest("[data-sidebar-tab-group]")!;
const tabHeader = tabButton.closest("[data-workbench-tab]")!;
const tabSurface = tabButton.closest("[data-workbench-tab-surface]")!;
@@ -587,7 +587,7 @@ describe("ChatList", () => {
expect(within(tabGroup).getByRole("button", {
name: "Expand panes in Root topic",
})).toHaveAttribute("aria-expanded", "false");
- expect(within(tabGroup).getByRole("button", { name: "Tab: Root topic" }))
+ expect(within(tabGroup).getByRole("button", { name: "Group: Root topic" }))
.not.toHaveAttribute("aria-current");
fireEvent.click(within(tabGroup).getByRole("button", {
@@ -634,8 +634,8 @@ describe("ChatList", () => {
fireEvent.click(await screen.findByRole("menuitem", { name: "Select" }));
expect(screen.getByText("1 selected")).toBeInTheDocument();
- fireEvent.click(screen.getByRole("button", { name: "Tab: Root topic" }));
- expect(screen.getByRole("button", { name: "Tab: Root topic" }))
+ fireEvent.click(screen.getByRole("button", { name: "Group: Root topic" }));
+ expect(screen.getByRole("button", { name: "Group: Root topic" }))
.toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("button", { name: "Root topic" }))
.toHaveAttribute("aria-pressed", "true");
@@ -942,12 +942,12 @@ describe("ChatList", () => {
};
const firstRender = render();
- fireEvent.click(screen.getByRole("button", { name: "Tab: Root topic" }));
+ fireEvent.click(screen.getByRole("button", { name: "Group: Root topic" }));
expect(screen.queryByRole("button", { name: "Research pane" })).not.toBeInTheDocument();
firstRender.unmount();
render();
- expect(screen.getByRole("button", { name: "Tab: Root topic" }))
+ expect(screen.getByRole("button", { name: "Group: Root topic" }))
.toHaveAttribute("aria-expanded", "false");
expect(screen.queryByRole("button", { name: "Research pane" })).not.toBeInTheDocument();
});
diff --git a/webui/src/tests/i18n.test.tsx b/webui/src/tests/i18n.test.tsx
index b3b5a68a5..2891cbd97 100644
--- a/webui/src/tests/i18n.test.tsx
+++ b/webui/src/tests/i18n.test.tsx
@@ -269,6 +269,22 @@ const LOCALIZED_NEW_SURFACE_KEYS = [
"chat.groups.yesterday",
"chat.groups.earlier",
"chat.groups.archived",
+ "workbench.tabAria",
+ "workbench.panesInTab",
+ "workbench.collapseTabGroup",
+ "workbench.expandTabGroup",
+ "workbench.dropPane",
+ "workbench.createGroup",
+ "workbench.moveTo",
+ "workbench.renameGroupTitle",
+ "workbench.renameGroupDescription",
+ "workbench.renameGroupPlaceholder",
+ "workbench.dissolveTab",
+ "workbench.deleteConversations",
+ "workbench.paneLimit",
+ "workbench.paneActions",
+ "workbench.detachPane",
+ "workbench.composerAria",
"thread.promptNavigator.railAria",
"thread.composer.mentions.cliTitle",
"thread.composer.mentions.mcpTitle",
@@ -582,6 +598,18 @@ describe("webui i18n", () => {
expect(settings.skills.marketplaceTrendingTitle).toBe("各市场热门技能");
});
+ it("keeps the Simplified Chinese group workflow localized", () => {
+ const workbench = resources["zh-CN"].common.workbench;
+
+ expect(workbench.tabAria).toBe("分组:{{title}}");
+ expect(workbench.createGroup).toBe("创建分组");
+ expect(workbench.renameGroupTitle).toBe("重命名分组");
+ expect(workbench.renameGroupDescription).toBe("为这个分组命名。");
+ expect(workbench.renameGroupPlaceholder).toBe("分组名称");
+ expect(workbench.moveTo).toBe("移动到");
+ expect(workbench.detachPane).toBe("移出");
+ });
+
it("keeps Indonesian and Vietnamese settings free of copied Spanish help text", () => {
const spanish = flattenResource(resources.es.common);