From ed15c70b1b13aa5e38cde518f11147abaf947720 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 12:13:51 +0800 Subject: [PATCH 1/7] fix(ui): localize the Astryx chrome adopted since #1795 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1795 moved the chat surfaces onto Astryx ChatLayout and deleted the `label={copy.jumpLatest}` that fed Maka's own scroll-to-bottom button, but never registered the replacement `@astryx.*` keys. Astryx ships no zh catalog, so the pill has been reading "New messages" in a Chinese UI ever since — and the same gap covers every other Astryx surface adopted without its copy. Audited all 219 shipped Astryx keys against the components Maka actually renders (JSX usage, not imports — the barrel re-export makes every component look reachable). 79 rendered keys had no override; this adds them, reusing existing entries where the string is identical rather than duplicating (form.clear, primitives.close/loading, selectPlaceholder). Excluded as dead config per this file's own rule: dateInput.dialogLabel and dateInput.placeholder — DateTimeInput references neither, and no standalone DateInput is rendered. The guard test listed `chat` as a surface Maka does not render, which is what let the regression through; it now guards lightbox only, and a new test pins the chat chrome. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- .../ui/src/__tests__/markdown-body.test.ts | 16 +- packages/ui/src/astryx-i18n.tsx | 93 +++++++ packages/ui/src/shared-ui-copy.ts | 239 ++++++++++++++++++ 3 files changed, 347 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts index 19a045f61f..04983929ea 100644 --- a/packages/ui/src/__tests__/markdown-body.test.ts +++ b/packages/ui/src/__tests__/markdown-body.test.ts @@ -219,17 +219,31 @@ it('localizes Astryx Markdown accessibility copy in Chinese', () => { assert.doesNotMatch(markup, />Checkbox { const messages = astryxMessageOverrides('zh')?.zh ?? {}; for (const key of Object.keys(messages)) { assert.doesNotMatch( key, - /^@astryx\.(?:lightbox|chat)/, + /^@astryx\.lightbox/, `dead Astryx locale override: ${key}`, ); } }); +it('localizes the Astryx chat chrome adopted in #1795', () => { + const messages = astryxMessageOverrides('zh')?.zh ?? {}; + assert.equal(messages['@astryx.chatLayout.newMessages'], '有新消息'); + assert.equal(messages['@astryx.chatLayoutScrollButton.scrollToBottom'], '滚动到底部'); + for (const key of ['@astryx.chatSendButton.send', '@astryx.chat.status.sent']) { + assert.doesNotMatch(messages[key] ?? '', /[A-Za-z]/, `untranslated: ${key}`); + } +}); + it('uses the localized Astryx code block and syntax tokenizer', () => { const markup = renderToStaticMarkup( createElement( diff --git a/packages/ui/src/astryx-i18n.tsx b/packages/ui/src/astryx-i18n.tsx index 6a073d85ba..6e4a87fbc9 100644 --- a/packages/ui/src/astryx-i18n.tsx +++ b/packages/ui/src/astryx-i18n.tsx @@ -56,6 +56,7 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined if (locale === 'en') return undefined; const shared = getSharedUiCopy(locale); const form = shared.formControls; + const a = shared.astryx; return { [locale]: { '@astryx.codeBlock.copyCode': shared.markdown.copyCode, @@ -75,6 +76,98 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined '@astryx.selector.placeholder': form.selectPlaceholder, '@astryx.selector.clearLabel': form.clear, '@astryx.numberInput.clearLabel': form.clear, + + // Chat — the transcript, composer and scroll affordances Astryx owns + // since #1795 moved the chat surfaces onto ChatLayout. + '@astryx.chat.composer.placeholder': a.chat.composerPlaceholder, + '@astryx.chat.composerDrawer.label': a.chat.composerDrawerLabel, + '@astryx.chat.composerInput.label': a.chat.composerInputLabel, + '@astryx.chat.messageAriaLabel': a.chat.messageAriaLabel, + '@astryx.chat.pastedText.expand': a.chat.pastedTextExpand, + '@astryx.chat.status.delivered': a.chat.statusDelivered, + '@astryx.chat.status.failed': a.chat.statusFailed, + '@astryx.chat.status.read': a.chat.statusRead, + '@astryx.chat.status.sending': a.chat.statusSending, + '@astryx.chat.status.sent': a.chat.statusSent, + '@astryx.chatComposerDrawer.collapse': a.chat.drawerCollapse, + '@astryx.chatComposerDrawer.expand': a.chat.drawerExpand, + '@astryx.chatLayout.newMessages': a.chat.newMessages, + '@astryx.chatLayoutScrollButton.scrollToBottom': a.chat.scrollToBottom, + '@astryx.chatSendButton.send': a.chat.send, + '@astryx.chatSendButton.stop': a.chat.stop, + '@astryx.chatToolCalls.error': a.chat.toolCallsError, + '@astryx.chatToolCalls.groupLabel': a.chat.toolCallsGroupLabel, + '@astryx.chatTriggerMenu.suggestions': a.chat.triggerSuggestions, + + // Command palette — `list.label` stays a call-site override because each + // palette names its own result list. + '@astryx.commandPalette.emptyBootstrap': a.commandPalette.emptyBootstrap, + '@astryx.commandPalette.emptySearch': a.commandPalette.emptySearch, + '@astryx.commandPalette.input.placeholder': a.commandPalette.inputPlaceholder, + '@astryx.commandPalette.label': a.commandPalette.label, + '@astryx.commandPalette.loading': shared.primitives.loading, + '@astryx.commandPalette.noResultsFor': a.commandPalette.noResultsFor, + '@astryx.commandPalette.resultCount': a.commandPalette.resultCount, + + // DateTimeInput and the Calendar it opens. + '@astryx.dateInput.clear': form.clear, + '@astryx.dateInput.closeCalendar': a.dateTime.closeCalendar, + '@astryx.dateInput.openCalendar': a.dateTime.openCalendar, + '@astryx.dateInput.toggleCalendarClose': a.dateTime.closeCalendar, + '@astryx.dateTimeInput.dialogLabel': a.dateTime.dialogLabel, + '@astryx.dateTimeInput.placeholder': a.dateTime.datePlaceholder, + '@astryx.dateTimeInput.timePlaceholder': a.dateTime.timePlaceholder, + '@astryx.dateTimeInput.timeSuffix': a.dateTime.timeSuffix, + '@astryx.calendar.dayInRange': a.calendar.dayInRange, + '@astryx.calendar.dayRangeEnd': a.calendar.dayRangeEnd, + '@astryx.calendar.dayRangeStart': a.calendar.dayRangeStart, + '@astryx.calendar.dayRangeStartAndEnd': a.calendar.dayRangeStartAndEnd, + '@astryx.calendar.daySelected': a.calendar.daySelected, + '@astryx.calendar.nextMonth': a.calendar.nextMonth, + '@astryx.calendar.previousMonth': a.calendar.previousMonth, + '@astryx.calendar.rangeCompleteAnnounce': a.calendar.rangeCompleteAnnounce, + '@astryx.calendar.rangeStartAnnounce': a.calendar.rangeStartAnnounce, + + // Menus, selectors and inputs. + '@astryx.dropdownMenu.label': a.menus.dropdown, + '@astryx.moreMenu.label': a.menus.more, + '@astryx.selector.searchOptions': a.search.options, + '@astryx.selector.searchPlaceholder': a.search.placeholder, + '@astryx.multiSelector.searchOptions': a.search.options, + '@astryx.multiSelector.searchPlaceholder': a.search.placeholder, + '@astryx.multiSelector.selectPlaceholder': form.selectPlaceholder, + '@astryx.multiSelector.clearAll': a.multiSelector.clearAll, + '@astryx.multiSelector.selectAll': a.multiSelector.selectAll, + '@astryx.textInput.clearLabel': form.clear, + '@astryx.input.statusButton.error': a.inputStatus.error, + '@astryx.input.statusButton.success': a.inputStatus.success, + '@astryx.input.statusButton.warning': a.inputStatus.warning, + + // Shell chrome: side nav, tabs, banners, breadcrumbs, resize handles. + '@astryx.appShell.mobileNavigation': a.appShell.mobileNavigation, + '@astryx.banner.collapse': a.banner.collapse, + '@astryx.banner.expand': a.banner.expand, + '@astryx.banner.dismiss': shared.primitives.close, + '@astryx.breadcrumbs.label': a.breadcrumbs.label, + '@astryx.sideNav.label': a.sideNav.label, + '@astryx.sideNav.resizeSidebar': a.sideNav.resizeSidebar, + '@astryx.sideNavCollapseButton.collapseSidebar': a.sideNav.collapseSidebar, + '@astryx.sideNavCollapseButton.expandSidebar': a.sideNav.expandSidebar, + '@astryx.sideNavItem.collapse': a.sideNav.itemCollapse, + '@astryx.sideNavItem.expand': a.sideNav.itemExpand, + '@astryx.tabList.label': a.tabList.label, + + // Table (usage settings) and the chat transcript's attachment chrome. + '@astryx.table.label': a.table.label, + '@astryx.table.noData': a.table.noData, + '@astryx.table.filter.allPlaceholder': a.table.filterAll, + '@astryx.table.filter.apply': a.table.filterApply, + '@astryx.table.filter.reset': a.table.filterReset, + '@astryx.tableFiltering.filterByColumn': a.table.filterByColumn, + '@astryx.thumbnail.fallbackName': a.thumbnail.fallbackName, + '@astryx.thumbnail.open': a.thumbnail.open, + '@astryx.thumbnail.remove': a.thumbnail.remove, + '@astryx.token.remove': a.token.remove, }, }; } diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index c36aa19b84..538694e080 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -102,6 +102,90 @@ export interface SharedUiCopy { }; artifact: { unknownSize: string }; providers: { minimaxChina: string; custom: string; claudeSubscription: string }; + /** + * Copy that exists only to fill Astryx's own message catalog, which ships no + * `zh`. Grouped by the component that renders it so a slice adopting a new + * Astryx surface can see at a glance whether its strings are already covered. + * `astryxMessageOverrides` is the only consumer — nothing here is rendered by + * Maka's own components. + */ + astryx: { + appShell: { mobileNavigation: string }; + banner: { collapse: string; expand: string }; + breadcrumbs: { label: string }; + calendar: { + dayInRange: string; + dayRangeEnd: string; + dayRangeStart: string; + dayRangeStartAndEnd: string; + daySelected: string; + nextMonth: string; + previousMonth: string; + rangeCompleteAnnounce: string; + rangeStartAnnounce: string; + }; + chat: { + composerPlaceholder: string; + composerDrawerLabel: string; + composerInputLabel: string; + messageAriaLabel: string; + pastedTextExpand: string; + statusDelivered: string; + statusFailed: string; + statusRead: string; + statusSending: string; + statusSent: string; + drawerCollapse: string; + drawerExpand: string; + newMessages: string; + scrollToBottom: string; + send: string; + stop: string; + toolCallsError: string; + toolCallsGroupLabel: string; + triggerSuggestions: string; + }; + commandPalette: { + emptyBootstrap: string; + emptySearch: string; + inputPlaceholder: string; + label: string; + noResultsFor: string; + resultCount: string; + }; + dateTime: { + closeCalendar: string; + openCalendar: string; + dialogLabel: string; + datePlaceholder: string; + timePlaceholder: string; + timeSuffix: string; + }; + inputStatus: { error: string; success: string; warning: string }; + menus: { dropdown: string; more: string }; + multiSelector: { clearAll: string; selectAll: string }; + /** Selector and MultiSelector render the same two search affordances. */ + search: { options: string; placeholder: string }; + sideNav: { + label: string; + resizeSidebar: string; + collapseSidebar: string; + expandSidebar: string; + itemCollapse: string; + itemExpand: string; + }; + tabList: { label: string }; + table: { + label: string; + noData: string; + filterAll: string; + filterApply: string; + filterReset: string; + filterByColumn: string; + }; + thumbnail: { fallbackName: string; open: string; remove: string }; + token: { remove: string }; + }; } const SHARED_UI_COPY = { @@ -192,6 +276,82 @@ const SHARED_UI_COPY = { stream: { assistantChunkTruncated: '\n[…单条 delta 已截断]\n', assistantTailTruncated: '\n\n[…后续已截断]', thinkingHeadTruncated: '[…已截断早期 reasoning]\n', thinkingChunkTruncated: '\n[…单条 delta 已截断]\n', toolChunkTruncated: '\n[…已截断]\n' }, artifact: { unknownSize: '未知大小' }, providers: { minimaxChina: 'MiniMax 中国站', custom: '自定义', claudeSubscription: 'Claude 订阅' }, + astryx: { + appShell: { mobileNavigation: '移动端导航' }, + banner: { collapse: '收起', expand: '展开' }, + breadcrumbs: { label: '面包屑导航' }, + calendar: { + dayInRange: '{date},在所选范围内', + dayRangeEnd: '{date},范围结束', + dayRangeStart: '{date},范围开始', + dayRangeStartAndEnd: '{date},范围开始与结束', + daySelected: '{date},已选择', + nextMonth: '下个月', + previousMonth: '上个月', + rangeCompleteAnnounce: '已选择范围:{start} 至 {end}。', + rangeStartAnnounce: '开始日期 {date}。请选择结束日期。', + }, + chat: { + composerPlaceholder: '输入消息…', + composerDrawerLabel: '附加内容', + composerInputLabel: '消息输入框', + messageAriaLabel: '消息:{status}', + pastedTextExpand: '展开', + statusDelivered: '已送达', + statusFailed: '发送失败', + statusRead: '已读', + statusSending: '发送中', + statusSent: '已发送', + drawerCollapse: '收起{label}', + drawerExpand: '展开{label}', + newMessages: '有新消息', + scrollToBottom: '滚动到底部', + send: '发送', + stop: '停止', + toolCallsError: '错误:{message}', + toolCallsGroupLabel: '{count} 次工具调用', + triggerSuggestions: '建议', + }, + commandPalette: { + emptyBootstrap: '输入以搜索', + emptySearch: '无结果', + inputPlaceholder: '搜索…', + label: '命令面板', + noResultsFor: '没有与「{query}」匹配的结果', + resultCount: '{count, number} 条结果', + }, + dateTime: { + closeCalendar: '关闭日历', + openCalendar: '打开日历', + dialogLabel: '选择日期', + datePlaceholder: '选择日期', + timePlaceholder: '选择时间', + timeSuffix: '{label}时间', + }, + inputStatus: { error: '错误详情', success: '成功详情', warning: '警告详情' }, + menus: { dropdown: '菜单', more: '更多选项' }, + multiSelector: { clearAll: '清除全部{label}', selectAll: '全选' }, + search: { options: '搜索选项', placeholder: '搜索…' }, + sideNav: { + label: '侧边导航', + resizeSidebar: '调整侧边栏宽度', + collapseSidebar: '收起侧边栏', + expandSidebar: '展开侧边栏', + itemCollapse: '收起{label}', + itemExpand: '展开{label}', + }, + tabList: { label: '标签页' }, + table: { + label: '表格', + noData: '暂无数据', + filterAll: '全部', + filterApply: '应用', + filterReset: '重置', + filterByColumn: '筛选{header}', + }, + thumbnail: { fallbackName: '缩略图', open: '打开{accessibleName}', remove: '移除{accessibleName}' }, + token: { remove: '移除{label}' }, + }, }, en: { capabilityAudit: { @@ -280,6 +440,85 @@ const SHARED_UI_COPY = { stream: { assistantChunkTruncated: '\n[…single delta truncated]\n', assistantTailTruncated: '\n\n[…remaining output truncated]', thinkingHeadTruncated: '[…earlier reasoning truncated]\n', thinkingChunkTruncated: '\n[…single delta truncated]\n', toolChunkTruncated: '\n[…truncated]\n' }, artifact: { unknownSize: 'Unknown size' }, providers: { minimaxChina: 'MiniMax China', custom: 'Custom', claudeSubscription: 'Claude subscription' }, + // Never applied — `astryxMessageOverrides` returns undefined for `en`, so + // Astryx resolves its own shipped catalog. Mirrored verbatim from that + // catalog so the two columns stay diffable when Astryx changes a default. + astryx: { + appShell: { mobileNavigation: 'Mobile navigation' }, + banner: { collapse: 'Collapse', expand: 'Expand' }, + breadcrumbs: { label: 'Breadcrumb' }, + calendar: { + dayInRange: '{date}, in range', + dayRangeEnd: '{date}, range end', + dayRangeStart: '{date}, range start', + dayRangeStartAndEnd: '{date}, range start and range end', + daySelected: '{date}, selected', + nextMonth: 'Next month', + previousMonth: 'Previous month', + rangeCompleteAnnounce: 'Selected range: {start} to {end}.', + rangeStartAnnounce: 'Start date {date}. Select an end date.', + }, + chat: { + composerPlaceholder: 'Type a message…', + composerDrawerLabel: 'Items', + composerInputLabel: 'Message input', + messageAriaLabel: 'Message {status}', + pastedTextExpand: 'Expand', + statusDelivered: 'Delivered', + statusFailed: 'Failed', + statusRead: 'Read', + statusSending: 'Sending', + statusSent: 'Sent', + drawerCollapse: 'Collapse {label}', + drawerExpand: 'Expand {label}', + newMessages: 'New messages', + scrollToBottom: 'Scroll to bottom', + send: 'Send', + stop: 'Stop', + toolCallsError: 'Error: {message}', + toolCallsGroupLabel: '{count} tool calls', + triggerSuggestions: 'Suggestions', + }, + commandPalette: { + emptyBootstrap: 'Type to search', + emptySearch: 'No results', + inputPlaceholder: 'Search…', + label: 'Command palette', + noResultsFor: 'No results for {query}', + resultCount: '{count, number} {count, plural, one {result} other {results}}', + }, + dateTime: { + closeCalendar: 'Close calendar', + openCalendar: 'Open calendar', + dialogLabel: 'Choose date', + datePlaceholder: 'Select a date', + timePlaceholder: 'Select a time', + timeSuffix: '{label} time', + }, + inputStatus: { error: 'Error details', success: 'Success details', warning: 'Warning details' }, + menus: { dropdown: 'Menu', more: 'More options' }, + multiSelector: { clearAll: 'Clear all {label}', selectAll: 'Select all' }, + search: { options: 'Search options', placeholder: 'Search…' }, + sideNav: { + label: 'Side navigation', + resizeSidebar: 'Resize sidebar', + collapseSidebar: 'Collapse sidebar', + expandSidebar: 'Expand sidebar', + itemCollapse: 'Collapse {label}', + itemExpand: 'Expand {label}', + }, + tabList: { label: 'Tabs' }, + table: { + label: 'Table', + noData: 'No data', + filterAll: 'All', + filterApply: 'Apply', + filterReset: 'Reset', + filterByColumn: 'Filter {header}', + }, + thumbnail: { fallbackName: 'Thumbnail', open: 'Open {accessibleName}', remove: 'Remove {accessibleName}' }, + token: { remove: 'Remove {label}' }, + }, }, } satisfies UiCatalog; From ca42259fc20f7ab6a351c8451f35d129b87f55f2 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 15:22:47 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix(ui):=20keep=20=E8=B7=B3=E5=88=B0?= =?UTF-8?q?=E6=9C=80=E6=96=B0=E6=B6=88=E6=81=AF=20for=20the=20scroll-to-bo?= =?UTF-8?q?ttom=20pill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astryx expands the pill only when new messages arrived, so the label reads as a notice rather than an action — but the copy this replaced (#1194's `jumpLatest`, dropped by #1795) said 跳到最新消息, and keeping the wording users already know beats a semantically tidier rename. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- packages/ui/src/__tests__/markdown-body.test.ts | 2 +- packages/ui/src/shared-ui-copy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts index 04983929ea..0f2aef889a 100644 --- a/packages/ui/src/__tests__/markdown-body.test.ts +++ b/packages/ui/src/__tests__/markdown-body.test.ts @@ -237,7 +237,7 @@ it('ships overrides only for Astryx surfaces Maka renders', () => { it('localizes the Astryx chat chrome adopted in #1795', () => { const messages = astryxMessageOverrides('zh')?.zh ?? {}; - assert.equal(messages['@astryx.chatLayout.newMessages'], '有新消息'); + assert.equal(messages['@astryx.chatLayout.newMessages'], '跳到最新消息'); assert.equal(messages['@astryx.chatLayoutScrollButton.scrollToBottom'], '滚动到底部'); for (const key of ['@astryx.chatSendButton.send', '@astryx.chat.status.sent']) { assert.doesNotMatch(messages[key] ?? '', /[A-Za-z]/, `untranslated: ${key}`); diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 538694e080..c23356a751 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -304,7 +304,7 @@ const SHARED_UI_COPY = { statusSent: '已发送', drawerCollapse: '收起{label}', drawerExpand: '展开{label}', - newMessages: '有新消息', + newMessages: '跳到最新消息', scrollToBottom: '滚动到底部', send: '发送', stop: '停止', From acead5ea0da55a94d5b14553f1f23253a9b0ae02 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 15:37:17 +0800 Subject: [PATCH 3/7] test(ui): assert the pinned chat overrides exist before checking them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `messages[key] ?? ''` coalesced a deleted override to the empty string, which holds no Latin letters and satisfied the translation check on its own — so the loop caught an English value but not a removed key. Verified both ways: deleting the send override now fails with 'missing override', and reverting its copy to English fails with 'untranslated'. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- packages/ui/src/__tests__/markdown-body.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts index 0f2aef889a..e29b9870bb 100644 --- a/packages/ui/src/__tests__/markdown-body.test.ts +++ b/packages/ui/src/__tests__/markdown-body.test.ts @@ -240,7 +240,11 @@ it('localizes the Astryx chat chrome adopted in #1795', () => { assert.equal(messages['@astryx.chatLayout.newMessages'], '跳到最新消息'); assert.equal(messages['@astryx.chatLayoutScrollButton.scrollToBottom'], '滚动到底部'); for (const key of ['@astryx.chatSendButton.send', '@astryx.chat.status.sent']) { - assert.doesNotMatch(messages[key] ?? '', /[A-Za-z]/, `untranslated: ${key}`); + // Assert presence first: a deleted key would coalesce to '', which holds no + // Latin letters and would satisfy the translation check on its own. + const value = messages[key]; + assert.ok(value, `missing override: ${key}`); + assert.doesNotMatch(value, /[A-Za-z]/, `untranslated: ${key}`); } }); From 1c71c6988588ac1b29348cc01117534583c75af6 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 15:45:11 +0800 Subject: [PATCH 4/7] fix(ui): localize the Lightbox reached via useLightbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dead-config guard's surviving entry was as stale as the one #1795 invalidated: chat-turn.tsx opens image previews through useLightbox, which renders Astryx's Lightbox — with four English strings — while a JSX-tag scan for reports the component unrendered. Add the four overrides (close reuses primitives.close) and retire the ban-list guard entirely: a list keyed to "what we render today" rots silently, and both of its entries had. Live surfaces are pinned by presence tests instead. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- .../ui/src/__tests__/markdown-body.test.ts | 38 +++++++++++-------- packages/ui/src/astryx-i18n.tsx | 7 ++++ packages/ui/src/shared-ui-copy.ts | 3 ++ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts index e29b9870bb..5a889606c9 100644 --- a/packages/ui/src/__tests__/markdown-body.test.ts +++ b/packages/ui/src/__tests__/markdown-body.test.ts @@ -219,22 +219,14 @@ it('localizes Astryx Markdown accessibility copy in Chinese', () => { assert.doesNotMatch(markup, />Checkbox { - const messages = astryxMessageOverrides('zh')?.zh ?? {}; - for (const key of Object.keys(messages)) { - assert.doesNotMatch( - key, - /^@astryx\.lightbox/, - `dead Astryx locale override: ${key}`, - ); - } -}); - +// A dead-config guard used to sit here, banning override keys for Astryx +// surfaces Maka supposedly never rendered. Both of its entries rotted the +// same way: `chat` stopped being true at #1795 (ChatLayout took over the +// transcript, and the guard then blocked the fix for the English +// scroll-to-bottom pill), and `lightbox` was never true — chat-turn.tsx +// reaches Lightbox through useLightbox, which a JSX-tag scan misses. A ban +// list keyed to "what we render today" goes stale silently, so it is gone; +// the tests below pin the surfaces we know are live instead. it('localizes the Astryx chat chrome adopted in #1795', () => { const messages = astryxMessageOverrides('zh')?.zh ?? {}; assert.equal(messages['@astryx.chatLayout.newMessages'], '跳到最新消息'); @@ -248,6 +240,20 @@ it('localizes the Astryx chat chrome adopted in #1795', () => { } }); +it('localizes the Lightbox reached via useLightbox in chat-turn', () => { + const messages = astryxMessageOverrides('zh')?.zh ?? {}; + for (const key of [ + '@astryx.lightbox.mediaViewer', + '@astryx.lightbox.close', + '@astryx.lightbox.previous', + '@astryx.lightbox.next', + ]) { + const value = messages[key]; + assert.ok(value, `missing override: ${key}`); + assert.doesNotMatch(value, /[A-Za-z]/, `untranslated: ${key}`); + } +}); + it('uses the localized Astryx code block and syntax tokenizer', () => { const markup = renderToStaticMarkup( createElement( diff --git a/packages/ui/src/astryx-i18n.tsx b/packages/ui/src/astryx-i18n.tsx index 6e4a87fbc9..514d80a034 100644 --- a/packages/ui/src/astryx-i18n.tsx +++ b/packages/ui/src/astryx-i18n.tsx @@ -143,6 +143,13 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined '@astryx.input.statusButton.success': a.inputStatus.success, '@astryx.input.statusButton.warning': a.inputStatus.warning, + // Lightbox — reached through useLightbox in chat-turn.tsx (image + // preview), not a element; JSX-tag scans miss it. + '@astryx.lightbox.mediaViewer': a.lightbox.mediaViewer, + '@astryx.lightbox.close': shared.primitives.close, + '@astryx.lightbox.previous': a.lightbox.previous, + '@astryx.lightbox.next': a.lightbox.next, + // Shell chrome: side nav, tabs, banners, breadcrumbs, resize handles. '@astryx.appShell.mobileNavigation': a.appShell.mobileNavigation, '@astryx.banner.collapse': a.banner.collapse, diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index c23356a751..06a1315c1b 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -162,6 +162,7 @@ export interface SharedUiCopy { timeSuffix: string; }; inputStatus: { error: string; success: string; warning: string }; + lightbox: { mediaViewer: string; previous: string; next: string }; menus: { dropdown: string; more: string }; multiSelector: { clearAll: string; selectAll: string }; /** Selector and MultiSelector render the same two search affordances. */ @@ -329,6 +330,7 @@ const SHARED_UI_COPY = { timeSuffix: '{label}时间', }, inputStatus: { error: '错误详情', success: '成功详情', warning: '警告详情' }, + lightbox: { mediaViewer: '媒体查看器', previous: '上一张', next: '下一张' }, menus: { dropdown: '菜单', more: '更多选项' }, multiSelector: { clearAll: '清除全部{label}', selectAll: '全选' }, search: { options: '搜索选项', placeholder: '搜索…' }, @@ -496,6 +498,7 @@ const SHARED_UI_COPY = { timeSuffix: '{label} time', }, inputStatus: { error: 'Error details', success: 'Success details', warning: 'Warning details' }, + lightbox: { mediaViewer: 'Media viewer', previous: 'Previous', next: 'Next' }, menus: { dropdown: 'Menu', more: 'More options' }, multiSelector: { clearAll: 'Clear all {label}', selectAll: 'Select all' }, search: { options: 'Search options', placeholder: 'Search…' }, From af064aaca961e647570fbd83020ba3af8cd05930 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 15:49:43 +0800 Subject: [PATCH 5/7] test(e2e): match the quote token remove button by its localized name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token remove button read 'Remove {label}' only because Astryx's Token had no zh override; now that @astryx.token.remove is localized the accessible name is 移除…, and the /^Remove / locator times out. The spec already addresses every other control by its Chinese name. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- apps/desktop/e2e/quote-companion.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/e2e/quote-companion.spec.ts b/apps/desktop/e2e/quote-companion.spec.ts index a364ceb012..234dfc097f 100644 --- a/apps/desktop/e2e/quote-companion.spec.ts +++ b/apps/desktop/e2e/quote-companion.spec.ts @@ -45,7 +45,7 @@ test('quote companion removes one staged quote, forks, answers, and cleans up on // Quiet composer stages quotes as drawer Tokens (Astryx Token + remove). const quoteTokens = panel.locator('.maka-composer-context-drawer .astryx-token'); await expect(quoteTokens).toHaveCount(2); - await quoteTokens.first().getByRole('button', { name: /^Remove / }).click(); + await quoteTokens.first().getByRole('button', { name: /^移除/ }).click(); await expect(quoteTokens).toHaveCount(1); // Full text authority is the companion panel list, not truncated token labels. From 3901e14180cba8d3973b486d71dee1bf8bd2a041 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 17:56:51 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix(ui):=20address=20review=20=E2=80=94=20o?= =?UTF-8?q?ff-barrel=20Astryx=20copy,=20localize=20#642=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on #2202, all verified before fixing: - The astryx catalogue expanded SharedUiCopy, which `export *`s through the package barrel — violating the README's off-barrel convention for symbols with no cross-package consumer. It now lives in astryx-copy.ts, deliberately unexported. - The en mirror existed only as a diffable reference and was never applied (astryxMessageOverrides returns undefined for en); dropped rather than left to drift against upstream. - `a` renamed to `astryx`; duplicated presence/Latin-letter assertions extracted into assertChineseAstryxOverrides. - P1: the #642 streaming fallback rendered a bare ChatMessage, which resolves Astryx's shipped 'Message from {sender}' — an English aria-label in a Chinese tree. It now reuses LocalizedChatMessage with the same assistantAriaLabel as the TurnView path. The regression test pins the exact trigger: wait indicators with zero turns — passing a liveTurn instead projects a real turn and takes the localized path, which is how the first draft of the test passed against the bug. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- .../__tests__/chat-view-empty-state.test.tsx | 25 ++ .../ui/src/__tests__/markdown-body.test.ts | 22 +- packages/ui/src/astryx-copy.ts | 173 +++++++++++++ packages/ui/src/astryx-i18n.tsx | 157 ++++++------ packages/ui/src/chat-turn.tsx | 2 +- packages/ui/src/chat-view.tsx | 14 +- packages/ui/src/shared-ui-copy.ts | 242 ------------------ 7 files changed, 300 insertions(+), 335 deletions(-) create mode 100644 packages/ui/src/astryx-copy.ts diff --git a/packages/ui/src/__tests__/chat-view-empty-state.test.tsx b/packages/ui/src/__tests__/chat-view-empty-state.test.tsx index 1b24e1ea84..479dbd5763 100644 --- a/packages/ui/src/__tests__/chat-view-empty-state.test.tsx +++ b/packages/ui/src/__tests__/chat-view-empty-state.test.tsx @@ -174,3 +174,28 @@ describe('ChatView sent inline references', () => { assert.equal(markup.match(/astryx-badge/g)?.length, 2); }); }); + +describe('ChatView #642 streaming fallback', () => { + // The fallback renders outside TurnView, so it must localize the message + // aria-label itself — a bare ChatMessage resolves Astryx's shipped + // "Message from {sender}" and leaks English into a Chinese a11y tree. + it('localizes the fallback assistant message aria-label', () => { + // No liveTurn: a live turn projects into `turns` and takes the localized + // TurnView path instead. The fallback needs streaming with zero turns — + // wait indicators alone — which is exactly the #642 replay window. + const markup = renderToStaticMarkup( + + undefined} + /> + , + ); + + assert.match(markup, /data-live-streaming="true"/); + assert.match(markup, /aria-label="Maka 的回答"/); + assert.doesNotMatch(markup, /Message from/); + }); +}); diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts index 5a889606c9..1aa3adaea4 100644 --- a/packages/ui/src/__tests__/markdown-body.test.ts +++ b/packages/ui/src/__tests__/markdown-body.test.ts @@ -227,31 +227,31 @@ it('localizes Astryx Markdown accessibility copy in Chinese', () => { // reaches Lightbox through useLightbox, which a JSX-tag scan misses. A ban // list keyed to "what we render today" goes stale silently, so it is gone; // the tests below pin the surfaces we know are live instead. -it('localizes the Astryx chat chrome adopted in #1795', () => { +function assertChineseAstryxOverrides(keys: readonly string[]) { const messages = astryxMessageOverrides('zh')?.zh ?? {}; - assert.equal(messages['@astryx.chatLayout.newMessages'], '跳到最新消息'); - assert.equal(messages['@astryx.chatLayoutScrollButton.scrollToBottom'], '滚动到底部'); - for (const key of ['@astryx.chatSendButton.send', '@astryx.chat.status.sent']) { + for (const key of keys) { // Assert presence first: a deleted key would coalesce to '', which holds no // Latin letters and would satisfy the translation check on its own. const value = messages[key]; assert.ok(value, `missing override: ${key}`); assert.doesNotMatch(value, /[A-Za-z]/, `untranslated: ${key}`); } +} + +it('localizes the Astryx chat chrome adopted in #1795', () => { + const messages = astryxMessageOverrides('zh')?.zh ?? {}; + assert.equal(messages['@astryx.chatLayout.newMessages'], '跳到最新消息'); + assert.equal(messages['@astryx.chatLayoutScrollButton.scrollToBottom'], '滚动到底部'); + assertChineseAstryxOverrides(['@astryx.chatSendButton.send', '@astryx.chat.status.sent']); }); it('localizes the Lightbox reached via useLightbox in chat-turn', () => { - const messages = astryxMessageOverrides('zh')?.zh ?? {}; - for (const key of [ + assertChineseAstryxOverrides([ '@astryx.lightbox.mediaViewer', '@astryx.lightbox.close', '@astryx.lightbox.previous', '@astryx.lightbox.next', - ]) { - const value = messages[key]; - assert.ok(value, `missing override: ${key}`); - assert.doesNotMatch(value, /[A-Za-z]/, `untranslated: ${key}`); - } + ]); }); it('uses the localized Astryx code block and syntax tokenizer', () => { diff --git a/packages/ui/src/astryx-copy.ts b/packages/ui/src/astryx-copy.ts new file mode 100644 index 0000000000..865f9b4dd3 --- /dev/null +++ b/packages/ui/src/astryx-copy.ts @@ -0,0 +1,173 @@ +/** + * Chinese copy for Astryx's own message catalog, which ships no `zh`: without + * an override every `@astryx.*` string falls back to the shipped `en` catalog + * silently. Grouped by the component that renders it so a slice adopting a new + * Astryx surface can see at a glance whether its strings are already covered. + * + * Deliberately NOT exported from the package barrel (`index.ts`): the only + * consumer is `astryxMessageOverrides` in `astryx-i18n.tsx`, and per the + * README's off-barrel convention a symbol earns barrel export only with a + * cross-package consumer. Strings that Maka's own components also render live + * in `shared-ui-copy.ts` instead and are referenced from the override map, so + * shared wording keeps one home. + * + * `zh` only: `astryxMessageOverrides` returns `undefined` for `en`, which + * resolves Astryx's shipped defaults — an `en` mirror here would be dead + * config drifting against upstream. + */ +export interface AstryxCopy { + appShell: { mobileNavigation: string }; + banner: { collapse: string; expand: string }; + breadcrumbs: { label: string }; + calendar: { + dayInRange: string; + dayRangeEnd: string; + dayRangeStart: string; + dayRangeStartAndEnd: string; + daySelected: string; + nextMonth: string; + previousMonth: string; + rangeCompleteAnnounce: string; + rangeStartAnnounce: string; + }; + chat: { + composerPlaceholder: string; + composerDrawerLabel: string; + composerInputLabel: string; + messageAriaLabel: string; + pastedTextExpand: string; + statusDelivered: string; + statusFailed: string; + statusRead: string; + statusSending: string; + statusSent: string; + drawerCollapse: string; + drawerExpand: string; + newMessages: string; + scrollToBottom: string; + send: string; + stop: string; + toolCallsError: string; + toolCallsGroupLabel: string; + triggerSuggestions: string; + }; + commandPalette: { + emptyBootstrap: string; + emptySearch: string; + inputPlaceholder: string; + label: string; + noResultsFor: string; + resultCount: string; + }; + dateTime: { + closeCalendar: string; + openCalendar: string; + dialogLabel: string; + datePlaceholder: string; + timePlaceholder: string; + timeSuffix: string; + }; + inputStatus: { error: string; success: string; warning: string }; + lightbox: { mediaViewer: string; previous: string; next: string }; + menus: { dropdown: string; more: string }; + multiSelector: { clearAll: string; selectAll: string }; + /** Selector and MultiSelector render the same two search affordances. */ + search: { options: string; placeholder: string }; + sideNav: { + label: string; + resizeSidebar: string; + collapseSidebar: string; + expandSidebar: string; + itemCollapse: string; + itemExpand: string; + }; + tabList: { label: string }; + table: { + label: string; + noData: string; + filterAll: string; + filterApply: string; + filterReset: string; + filterByColumn: string; + }; + thumbnail: { fallbackName: string; open: string; remove: string }; + token: { remove: string }; +} + +export const ASTRYX_COPY_ZH: AstryxCopy = { + appShell: { mobileNavigation: '移动端导航' }, + banner: { collapse: '收起', expand: '展开' }, + breadcrumbs: { label: '面包屑导航' }, + calendar: { + dayInRange: '{date},在所选范围内', + dayRangeEnd: '{date},范围结束', + dayRangeStart: '{date},范围开始', + dayRangeStartAndEnd: '{date},范围开始与结束', + daySelected: '{date},已选择', + nextMonth: '下个月', + previousMonth: '上个月', + rangeCompleteAnnounce: '已选择范围:{start} 至 {end}。', + rangeStartAnnounce: '开始日期 {date}。请选择结束日期。', + }, + chat: { + composerPlaceholder: '输入消息…', + composerDrawerLabel: '附加内容', + composerInputLabel: '消息输入框', + messageAriaLabel: '消息:{status}', + pastedTextExpand: '展开', + statusDelivered: '已送达', + statusFailed: '发送失败', + statusRead: '已读', + statusSending: '发送中', + statusSent: '已发送', + drawerCollapse: '收起{label}', + drawerExpand: '展开{label}', + newMessages: '跳到最新消息', + scrollToBottom: '滚动到底部', + send: '发送', + stop: '停止', + toolCallsError: '错误:{message}', + toolCallsGroupLabel: '{count} 次工具调用', + triggerSuggestions: '建议', + }, + commandPalette: { + emptyBootstrap: '输入以搜索', + emptySearch: '无结果', + inputPlaceholder: '搜索…', + label: '命令面板', + noResultsFor: '没有与「{query}」匹配的结果', + resultCount: '{count, number} 条结果', + }, + dateTime: { + closeCalendar: '关闭日历', + openCalendar: '打开日历', + dialogLabel: '选择日期', + datePlaceholder: '选择日期', + timePlaceholder: '选择时间', + timeSuffix: '{label}时间', + }, + inputStatus: { error: '错误详情', success: '成功详情', warning: '警告详情' }, + lightbox: { mediaViewer: '媒体查看器', previous: '上一张', next: '下一张' }, + menus: { dropdown: '菜单', more: '更多选项' }, + multiSelector: { clearAll: '清除全部{label}', selectAll: '全选' }, + search: { options: '搜索选项', placeholder: '搜索…' }, + sideNav: { + label: '侧边导航', + resizeSidebar: '调整侧边栏宽度', + collapseSidebar: '收起侧边栏', + expandSidebar: '展开侧边栏', + itemCollapse: '收起{label}', + itemExpand: '展开{label}', + }, + tabList: { label: '标签页' }, + table: { + label: '表格', + noData: '暂无数据', + filterAll: '全部', + filterApply: '应用', + filterReset: '重置', + filterByColumn: '筛选{header}', + }, + thumbnail: { fallbackName: '缩略图', open: '打开{accessibleName}', remove: '移除{accessibleName}' }, + token: { remove: '移除{label}' }, +}; diff --git a/packages/ui/src/astryx-i18n.tsx b/packages/ui/src/astryx-i18n.tsx index 514d80a034..d4c6e9b667 100644 --- a/packages/ui/src/astryx-i18n.tsx +++ b/packages/ui/src/astryx-i18n.tsx @@ -2,6 +2,7 @@ import { useMemo, type ReactNode } from 'react'; import { InternationalizationProvider } from '@astryxdesign/core/i18n'; import type { Overrides } from '@astryxdesign/core/i18n'; import { getSharedUiCopy } from './shared-ui-copy.js'; +import { ASTRYX_COPY_ZH } from './astryx-copy.js'; import { useUiLocale } from './locale-context.js'; import type { UiLocale } from './locale-helpers.js'; @@ -56,7 +57,9 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined if (locale === 'en') return undefined; const shared = getSharedUiCopy(locale); const form = shared.formControls; - const a = shared.astryx; + // `locale` is narrowed to 'zh' past the early return; the catalogue lives + // off-barrel in astryx-copy.ts because nothing outside this map consumes it. + const astryx = ASTRYX_COPY_ZH; return { [locale]: { '@astryx.codeBlock.copyCode': shared.markdown.copyCode, @@ -79,102 +82,102 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined // Chat — the transcript, composer and scroll affordances Astryx owns // since #1795 moved the chat surfaces onto ChatLayout. - '@astryx.chat.composer.placeholder': a.chat.composerPlaceholder, - '@astryx.chat.composerDrawer.label': a.chat.composerDrawerLabel, - '@astryx.chat.composerInput.label': a.chat.composerInputLabel, - '@astryx.chat.messageAriaLabel': a.chat.messageAriaLabel, - '@astryx.chat.pastedText.expand': a.chat.pastedTextExpand, - '@astryx.chat.status.delivered': a.chat.statusDelivered, - '@astryx.chat.status.failed': a.chat.statusFailed, - '@astryx.chat.status.read': a.chat.statusRead, - '@astryx.chat.status.sending': a.chat.statusSending, - '@astryx.chat.status.sent': a.chat.statusSent, - '@astryx.chatComposerDrawer.collapse': a.chat.drawerCollapse, - '@astryx.chatComposerDrawer.expand': a.chat.drawerExpand, - '@astryx.chatLayout.newMessages': a.chat.newMessages, - '@astryx.chatLayoutScrollButton.scrollToBottom': a.chat.scrollToBottom, - '@astryx.chatSendButton.send': a.chat.send, - '@astryx.chatSendButton.stop': a.chat.stop, - '@astryx.chatToolCalls.error': a.chat.toolCallsError, - '@astryx.chatToolCalls.groupLabel': a.chat.toolCallsGroupLabel, - '@astryx.chatTriggerMenu.suggestions': a.chat.triggerSuggestions, + '@astryx.chat.composer.placeholder': astryx.chat.composerPlaceholder, + '@astryx.chat.composerDrawer.label': astryx.chat.composerDrawerLabel, + '@astryx.chat.composerInput.label': astryx.chat.composerInputLabel, + '@astryx.chat.messageAriaLabel': astryx.chat.messageAriaLabel, + '@astryx.chat.pastedText.expand': astryx.chat.pastedTextExpand, + '@astryx.chat.status.delivered': astryx.chat.statusDelivered, + '@astryx.chat.status.failed': astryx.chat.statusFailed, + '@astryx.chat.status.read': astryx.chat.statusRead, + '@astryx.chat.status.sending': astryx.chat.statusSending, + '@astryx.chat.status.sent': astryx.chat.statusSent, + '@astryx.chatComposerDrawer.collapse': astryx.chat.drawerCollapse, + '@astryx.chatComposerDrawer.expand': astryx.chat.drawerExpand, + '@astryx.chatLayout.newMessages': astryx.chat.newMessages, + '@astryx.chatLayoutScrollButton.scrollToBottom': astryx.chat.scrollToBottom, + '@astryx.chatSendButton.send': astryx.chat.send, + '@astryx.chatSendButton.stop': astryx.chat.stop, + '@astryx.chatToolCalls.error': astryx.chat.toolCallsError, + '@astryx.chatToolCalls.groupLabel': astryx.chat.toolCallsGroupLabel, + '@astryx.chatTriggerMenu.suggestions': astryx.chat.triggerSuggestions, // Command palette — `list.label` stays a call-site override because each // palette names its own result list. - '@astryx.commandPalette.emptyBootstrap': a.commandPalette.emptyBootstrap, - '@astryx.commandPalette.emptySearch': a.commandPalette.emptySearch, - '@astryx.commandPalette.input.placeholder': a.commandPalette.inputPlaceholder, - '@astryx.commandPalette.label': a.commandPalette.label, + '@astryx.commandPalette.emptyBootstrap': astryx.commandPalette.emptyBootstrap, + '@astryx.commandPalette.emptySearch': astryx.commandPalette.emptySearch, + '@astryx.commandPalette.input.placeholder': astryx.commandPalette.inputPlaceholder, + '@astryx.commandPalette.label': astryx.commandPalette.label, '@astryx.commandPalette.loading': shared.primitives.loading, - '@astryx.commandPalette.noResultsFor': a.commandPalette.noResultsFor, - '@astryx.commandPalette.resultCount': a.commandPalette.resultCount, + '@astryx.commandPalette.noResultsFor': astryx.commandPalette.noResultsFor, + '@astryx.commandPalette.resultCount': astryx.commandPalette.resultCount, // DateTimeInput and the Calendar it opens. '@astryx.dateInput.clear': form.clear, - '@astryx.dateInput.closeCalendar': a.dateTime.closeCalendar, - '@astryx.dateInput.openCalendar': a.dateTime.openCalendar, - '@astryx.dateInput.toggleCalendarClose': a.dateTime.closeCalendar, - '@astryx.dateTimeInput.dialogLabel': a.dateTime.dialogLabel, - '@astryx.dateTimeInput.placeholder': a.dateTime.datePlaceholder, - '@astryx.dateTimeInput.timePlaceholder': a.dateTime.timePlaceholder, - '@astryx.dateTimeInput.timeSuffix': a.dateTime.timeSuffix, - '@astryx.calendar.dayInRange': a.calendar.dayInRange, - '@astryx.calendar.dayRangeEnd': a.calendar.dayRangeEnd, - '@astryx.calendar.dayRangeStart': a.calendar.dayRangeStart, - '@astryx.calendar.dayRangeStartAndEnd': a.calendar.dayRangeStartAndEnd, - '@astryx.calendar.daySelected': a.calendar.daySelected, - '@astryx.calendar.nextMonth': a.calendar.nextMonth, - '@astryx.calendar.previousMonth': a.calendar.previousMonth, - '@astryx.calendar.rangeCompleteAnnounce': a.calendar.rangeCompleteAnnounce, - '@astryx.calendar.rangeStartAnnounce': a.calendar.rangeStartAnnounce, + '@astryx.dateInput.closeCalendar': astryx.dateTime.closeCalendar, + '@astryx.dateInput.openCalendar': astryx.dateTime.openCalendar, + '@astryx.dateInput.toggleCalendarClose': astryx.dateTime.closeCalendar, + '@astryx.dateTimeInput.dialogLabel': astryx.dateTime.dialogLabel, + '@astryx.dateTimeInput.placeholder': astryx.dateTime.datePlaceholder, + '@astryx.dateTimeInput.timePlaceholder': astryx.dateTime.timePlaceholder, + '@astryx.dateTimeInput.timeSuffix': astryx.dateTime.timeSuffix, + '@astryx.calendar.dayInRange': astryx.calendar.dayInRange, + '@astryx.calendar.dayRangeEnd': astryx.calendar.dayRangeEnd, + '@astryx.calendar.dayRangeStart': astryx.calendar.dayRangeStart, + '@astryx.calendar.dayRangeStartAndEnd': astryx.calendar.dayRangeStartAndEnd, + '@astryx.calendar.daySelected': astryx.calendar.daySelected, + '@astryx.calendar.nextMonth': astryx.calendar.nextMonth, + '@astryx.calendar.previousMonth': astryx.calendar.previousMonth, + '@astryx.calendar.rangeCompleteAnnounce': astryx.calendar.rangeCompleteAnnounce, + '@astryx.calendar.rangeStartAnnounce': astryx.calendar.rangeStartAnnounce, // Menus, selectors and inputs. - '@astryx.dropdownMenu.label': a.menus.dropdown, - '@astryx.moreMenu.label': a.menus.more, - '@astryx.selector.searchOptions': a.search.options, - '@astryx.selector.searchPlaceholder': a.search.placeholder, - '@astryx.multiSelector.searchOptions': a.search.options, - '@astryx.multiSelector.searchPlaceholder': a.search.placeholder, + '@astryx.dropdownMenu.label': astryx.menus.dropdown, + '@astryx.moreMenu.label': astryx.menus.more, + '@astryx.selector.searchOptions': astryx.search.options, + '@astryx.selector.searchPlaceholder': astryx.search.placeholder, + '@astryx.multiSelector.searchOptions': astryx.search.options, + '@astryx.multiSelector.searchPlaceholder': astryx.search.placeholder, '@astryx.multiSelector.selectPlaceholder': form.selectPlaceholder, - '@astryx.multiSelector.clearAll': a.multiSelector.clearAll, - '@astryx.multiSelector.selectAll': a.multiSelector.selectAll, + '@astryx.multiSelector.clearAll': astryx.multiSelector.clearAll, + '@astryx.multiSelector.selectAll': astryx.multiSelector.selectAll, '@astryx.textInput.clearLabel': form.clear, - '@astryx.input.statusButton.error': a.inputStatus.error, - '@astryx.input.statusButton.success': a.inputStatus.success, - '@astryx.input.statusButton.warning': a.inputStatus.warning, + '@astryx.input.statusButton.error': astryx.inputStatus.error, + '@astryx.input.statusButton.success': astryx.inputStatus.success, + '@astryx.input.statusButton.warning': astryx.inputStatus.warning, // Lightbox — reached through useLightbox in chat-turn.tsx (image // preview), not a element; JSX-tag scans miss it. - '@astryx.lightbox.mediaViewer': a.lightbox.mediaViewer, + '@astryx.lightbox.mediaViewer': astryx.lightbox.mediaViewer, '@astryx.lightbox.close': shared.primitives.close, - '@astryx.lightbox.previous': a.lightbox.previous, - '@astryx.lightbox.next': a.lightbox.next, + '@astryx.lightbox.previous': astryx.lightbox.previous, + '@astryx.lightbox.next': astryx.lightbox.next, // Shell chrome: side nav, tabs, banners, breadcrumbs, resize handles. - '@astryx.appShell.mobileNavigation': a.appShell.mobileNavigation, - '@astryx.banner.collapse': a.banner.collapse, - '@astryx.banner.expand': a.banner.expand, + '@astryx.appShell.mobileNavigation': astryx.appShell.mobileNavigation, + '@astryx.banner.collapse': astryx.banner.collapse, + '@astryx.banner.expand': astryx.banner.expand, '@astryx.banner.dismiss': shared.primitives.close, - '@astryx.breadcrumbs.label': a.breadcrumbs.label, - '@astryx.sideNav.label': a.sideNav.label, - '@astryx.sideNav.resizeSidebar': a.sideNav.resizeSidebar, - '@astryx.sideNavCollapseButton.collapseSidebar': a.sideNav.collapseSidebar, - '@astryx.sideNavCollapseButton.expandSidebar': a.sideNav.expandSidebar, - '@astryx.sideNavItem.collapse': a.sideNav.itemCollapse, - '@astryx.sideNavItem.expand': a.sideNav.itemExpand, - '@astryx.tabList.label': a.tabList.label, + '@astryx.breadcrumbs.label': astryx.breadcrumbs.label, + '@astryx.sideNav.label': astryx.sideNav.label, + '@astryx.sideNav.resizeSidebar': astryx.sideNav.resizeSidebar, + '@astryx.sideNavCollapseButton.collapseSidebar': astryx.sideNav.collapseSidebar, + '@astryx.sideNavCollapseButton.expandSidebar': astryx.sideNav.expandSidebar, + '@astryx.sideNavItem.collapse': astryx.sideNav.itemCollapse, + '@astryx.sideNavItem.expand': astryx.sideNav.itemExpand, + '@astryx.tabList.label': astryx.tabList.label, // Table (usage settings) and the chat transcript's attachment chrome. - '@astryx.table.label': a.table.label, - '@astryx.table.noData': a.table.noData, - '@astryx.table.filter.allPlaceholder': a.table.filterAll, - '@astryx.table.filter.apply': a.table.filterApply, - '@astryx.table.filter.reset': a.table.filterReset, - '@astryx.tableFiltering.filterByColumn': a.table.filterByColumn, - '@astryx.thumbnail.fallbackName': a.thumbnail.fallbackName, - '@astryx.thumbnail.open': a.thumbnail.open, - '@astryx.thumbnail.remove': a.thumbnail.remove, - '@astryx.token.remove': a.token.remove, + '@astryx.table.label': astryx.table.label, + '@astryx.table.noData': astryx.table.noData, + '@astryx.table.filter.allPlaceholder': astryx.table.filterAll, + '@astryx.table.filter.apply': astryx.table.filterApply, + '@astryx.table.filter.reset': astryx.table.filterReset, + '@astryx.tableFiltering.filterByColumn': astryx.table.filterByColumn, + '@astryx.thumbnail.fallbackName': astryx.thumbnail.fallbackName, + '@astryx.thumbnail.open': astryx.thumbnail.open, + '@astryx.thumbnail.remove': astryx.thumbnail.remove, + '@astryx.token.remove': astryx.token.remove, }, }; } diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index 00e5c2a54d..51a0875a52 100644 --- a/packages/ui/src/chat-turn.tsx +++ b/packages/ui/src/chat-turn.tsx @@ -43,7 +43,7 @@ import { getConversationCopy } from './conversation-copy.js'; import { AstryxLocaleProvider } from './astryx-i18n.js'; import { InlineReferenceText } from './inline-reference.js'; -function LocalizedChatMessage({ +export function LocalizedChatMessage({ accessibleLabel, ...props }: Omit, 'aria-label'> & { diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx index 33e71ae164..d79a48b1d0 100644 --- a/packages/ui/src/chat-view.tsx +++ b/packages/ui/src/chat-view.tsx @@ -16,7 +16,7 @@ import type { StoredMessage, } from '@maka/core'; import { isDeepResearchSession } from '@maka/core'; -import { Button, ButtonGroup, ChatMessage, ChatMessageList, EmptyState } from '@astryxdesign/core'; +import { Button, ButtonGroup, ChatMessageList, EmptyState } from '@astryxdesign/core'; import { useChatLayoutContext } from '@astryxdesign/core/Chat'; import { useLayer } from '@astryxdesign/core/Layer'; import { materializeChat } from './materialize.js'; @@ -25,6 +25,7 @@ import type { LiveTurnProjection } from './live-turn-projection.js'; import { ModelContinuingIndicator, ModelProviderRetryIndicator, + LocalizedChatMessage, ModelProcessingIndicator, TurnView, type ReadAttachmentBytes, @@ -214,7 +215,8 @@ export function ChatView(props: { */ onAskAboutSelection?(input: { text: string; turnId?: string }): void; }) { - const copy = getConversationCopy(useUiLocale()).chat; + const conversationCopy = getConversationCopy(useUiLocale()); + const copy = conversationCopy.chat; // chat survives for the empty-state path; the main message log is driven by // `turns` (per @kenji UI-04 turn-grouping projection). const drainingMessageIdsKey = JSON.stringify( @@ -536,7 +538,11 @@ export function ChatView(props: { `tailTurnId` is undefined), so the answer never double-renders. */} {streamingActive && !tailTurnId && (
- +
{props.liveTurn?.providerRetry ? ( @@ -548,7 +554,7 @@ export function ChatView(props: { )}
)} {conversationItems diff --git a/packages/ui/src/shared-ui-copy.ts b/packages/ui/src/shared-ui-copy.ts index 06a1315c1b..c36aa19b84 100644 --- a/packages/ui/src/shared-ui-copy.ts +++ b/packages/ui/src/shared-ui-copy.ts @@ -102,91 +102,6 @@ export interface SharedUiCopy { }; artifact: { unknownSize: string }; providers: { minimaxChina: string; custom: string; claudeSubscription: string }; - /** - * Copy that exists only to fill Astryx's own message catalog, which ships no - * `zh`. Grouped by the component that renders it so a slice adopting a new - * Astryx surface can see at a glance whether its strings are already covered. - * `astryxMessageOverrides` is the only consumer — nothing here is rendered by - * Maka's own components. - */ - astryx: { - appShell: { mobileNavigation: string }; - banner: { collapse: string; expand: string }; - breadcrumbs: { label: string }; - calendar: { - dayInRange: string; - dayRangeEnd: string; - dayRangeStart: string; - dayRangeStartAndEnd: string; - daySelected: string; - nextMonth: string; - previousMonth: string; - rangeCompleteAnnounce: string; - rangeStartAnnounce: string; - }; - chat: { - composerPlaceholder: string; - composerDrawerLabel: string; - composerInputLabel: string; - messageAriaLabel: string; - pastedTextExpand: string; - statusDelivered: string; - statusFailed: string; - statusRead: string; - statusSending: string; - statusSent: string; - drawerCollapse: string; - drawerExpand: string; - newMessages: string; - scrollToBottom: string; - send: string; - stop: string; - toolCallsError: string; - toolCallsGroupLabel: string; - triggerSuggestions: string; - }; - commandPalette: { - emptyBootstrap: string; - emptySearch: string; - inputPlaceholder: string; - label: string; - noResultsFor: string; - resultCount: string; - }; - dateTime: { - closeCalendar: string; - openCalendar: string; - dialogLabel: string; - datePlaceholder: string; - timePlaceholder: string; - timeSuffix: string; - }; - inputStatus: { error: string; success: string; warning: string }; - lightbox: { mediaViewer: string; previous: string; next: string }; - menus: { dropdown: string; more: string }; - multiSelector: { clearAll: string; selectAll: string }; - /** Selector and MultiSelector render the same two search affordances. */ - search: { options: string; placeholder: string }; - sideNav: { - label: string; - resizeSidebar: string; - collapseSidebar: string; - expandSidebar: string; - itemCollapse: string; - itemExpand: string; - }; - tabList: { label: string }; - table: { - label: string; - noData: string; - filterAll: string; - filterApply: string; - filterReset: string; - filterByColumn: string; - }; - thumbnail: { fallbackName: string; open: string; remove: string }; - token: { remove: string }; - }; } const SHARED_UI_COPY = { @@ -277,83 +192,6 @@ const SHARED_UI_COPY = { stream: { assistantChunkTruncated: '\n[…单条 delta 已截断]\n', assistantTailTruncated: '\n\n[…后续已截断]', thinkingHeadTruncated: '[…已截断早期 reasoning]\n', thinkingChunkTruncated: '\n[…单条 delta 已截断]\n', toolChunkTruncated: '\n[…已截断]\n' }, artifact: { unknownSize: '未知大小' }, providers: { minimaxChina: 'MiniMax 中国站', custom: '自定义', claudeSubscription: 'Claude 订阅' }, - astryx: { - appShell: { mobileNavigation: '移动端导航' }, - banner: { collapse: '收起', expand: '展开' }, - breadcrumbs: { label: '面包屑导航' }, - calendar: { - dayInRange: '{date},在所选范围内', - dayRangeEnd: '{date},范围结束', - dayRangeStart: '{date},范围开始', - dayRangeStartAndEnd: '{date},范围开始与结束', - daySelected: '{date},已选择', - nextMonth: '下个月', - previousMonth: '上个月', - rangeCompleteAnnounce: '已选择范围:{start} 至 {end}。', - rangeStartAnnounce: '开始日期 {date}。请选择结束日期。', - }, - chat: { - composerPlaceholder: '输入消息…', - composerDrawerLabel: '附加内容', - composerInputLabel: '消息输入框', - messageAriaLabel: '消息:{status}', - pastedTextExpand: '展开', - statusDelivered: '已送达', - statusFailed: '发送失败', - statusRead: '已读', - statusSending: '发送中', - statusSent: '已发送', - drawerCollapse: '收起{label}', - drawerExpand: '展开{label}', - newMessages: '跳到最新消息', - scrollToBottom: '滚动到底部', - send: '发送', - stop: '停止', - toolCallsError: '错误:{message}', - toolCallsGroupLabel: '{count} 次工具调用', - triggerSuggestions: '建议', - }, - commandPalette: { - emptyBootstrap: '输入以搜索', - emptySearch: '无结果', - inputPlaceholder: '搜索…', - label: '命令面板', - noResultsFor: '没有与「{query}」匹配的结果', - resultCount: '{count, number} 条结果', - }, - dateTime: { - closeCalendar: '关闭日历', - openCalendar: '打开日历', - dialogLabel: '选择日期', - datePlaceholder: '选择日期', - timePlaceholder: '选择时间', - timeSuffix: '{label}时间', - }, - inputStatus: { error: '错误详情', success: '成功详情', warning: '警告详情' }, - lightbox: { mediaViewer: '媒体查看器', previous: '上一张', next: '下一张' }, - menus: { dropdown: '菜单', more: '更多选项' }, - multiSelector: { clearAll: '清除全部{label}', selectAll: '全选' }, - search: { options: '搜索选项', placeholder: '搜索…' }, - sideNav: { - label: '侧边导航', - resizeSidebar: '调整侧边栏宽度', - collapseSidebar: '收起侧边栏', - expandSidebar: '展开侧边栏', - itemCollapse: '收起{label}', - itemExpand: '展开{label}', - }, - tabList: { label: '标签页' }, - table: { - label: '表格', - noData: '暂无数据', - filterAll: '全部', - filterApply: '应用', - filterReset: '重置', - filterByColumn: '筛选{header}', - }, - thumbnail: { fallbackName: '缩略图', open: '打开{accessibleName}', remove: '移除{accessibleName}' }, - token: { remove: '移除{label}' }, - }, }, en: { capabilityAudit: { @@ -442,86 +280,6 @@ const SHARED_UI_COPY = { stream: { assistantChunkTruncated: '\n[…single delta truncated]\n', assistantTailTruncated: '\n\n[…remaining output truncated]', thinkingHeadTruncated: '[…earlier reasoning truncated]\n', thinkingChunkTruncated: '\n[…single delta truncated]\n', toolChunkTruncated: '\n[…truncated]\n' }, artifact: { unknownSize: 'Unknown size' }, providers: { minimaxChina: 'MiniMax China', custom: 'Custom', claudeSubscription: 'Claude subscription' }, - // Never applied — `astryxMessageOverrides` returns undefined for `en`, so - // Astryx resolves its own shipped catalog. Mirrored verbatim from that - // catalog so the two columns stay diffable when Astryx changes a default. - astryx: { - appShell: { mobileNavigation: 'Mobile navigation' }, - banner: { collapse: 'Collapse', expand: 'Expand' }, - breadcrumbs: { label: 'Breadcrumb' }, - calendar: { - dayInRange: '{date}, in range', - dayRangeEnd: '{date}, range end', - dayRangeStart: '{date}, range start', - dayRangeStartAndEnd: '{date}, range start and range end', - daySelected: '{date}, selected', - nextMonth: 'Next month', - previousMonth: 'Previous month', - rangeCompleteAnnounce: 'Selected range: {start} to {end}.', - rangeStartAnnounce: 'Start date {date}. Select an end date.', - }, - chat: { - composerPlaceholder: 'Type a message…', - composerDrawerLabel: 'Items', - composerInputLabel: 'Message input', - messageAriaLabel: 'Message {status}', - pastedTextExpand: 'Expand', - statusDelivered: 'Delivered', - statusFailed: 'Failed', - statusRead: 'Read', - statusSending: 'Sending', - statusSent: 'Sent', - drawerCollapse: 'Collapse {label}', - drawerExpand: 'Expand {label}', - newMessages: 'New messages', - scrollToBottom: 'Scroll to bottom', - send: 'Send', - stop: 'Stop', - toolCallsError: 'Error: {message}', - toolCallsGroupLabel: '{count} tool calls', - triggerSuggestions: 'Suggestions', - }, - commandPalette: { - emptyBootstrap: 'Type to search', - emptySearch: 'No results', - inputPlaceholder: 'Search…', - label: 'Command palette', - noResultsFor: 'No results for {query}', - resultCount: '{count, number} {count, plural, one {result} other {results}}', - }, - dateTime: { - closeCalendar: 'Close calendar', - openCalendar: 'Open calendar', - dialogLabel: 'Choose date', - datePlaceholder: 'Select a date', - timePlaceholder: 'Select a time', - timeSuffix: '{label} time', - }, - inputStatus: { error: 'Error details', success: 'Success details', warning: 'Warning details' }, - lightbox: { mediaViewer: 'Media viewer', previous: 'Previous', next: 'Next' }, - menus: { dropdown: 'Menu', more: 'More options' }, - multiSelector: { clearAll: 'Clear all {label}', selectAll: 'Select all' }, - search: { options: 'Search options', placeholder: 'Search…' }, - sideNav: { - label: 'Side navigation', - resizeSidebar: 'Resize sidebar', - collapseSidebar: 'Collapse sidebar', - expandSidebar: 'Expand sidebar', - itemCollapse: 'Collapse {label}', - itemExpand: 'Expand {label}', - }, - tabList: { label: 'Tabs' }, - table: { - label: 'Table', - noData: 'No data', - filterAll: 'All', - filterApply: 'Apply', - filterReset: 'Reset', - filterByColumn: 'Filter {header}', - }, - thumbnail: { fallbackName: 'Thumbnail', open: 'Open {accessibleName}', remove: 'Remove {accessibleName}' }, - token: { remove: 'Remove {label}' }, - }, }, } satisfies UiCatalog; From 47d74dd2a39a68a2a7577692268c7554160ecbb0 Mon Sep 17 00:00:00 2001 From: GabrielDrapor Date: Wed, 5 Aug 2026 18:20:30 +0800 Subject: [PATCH 7/7] fix(ui): prune dead overrides, sweep the whole zh map in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintainer review on #2202, each claim verified before acting: - chatSendButton.send/stop: ChatComposerInput never renders ChatSendButton — composer.tsx supplies its own send button. Pruned; the pinned test now anchors on chatToolCalls.error instead. - appShell.mobileNavigation: gated on shouldShowAutoToggle, and Maka passes breakpoint 'none' + hasToggle false. Pruned. - table.noData / table.filter.* / tableFiltering.filterByColumn: the usage table mounts only the rowHeader plugin and renders EmptyState before Table when rows are empty. Pruned. - New whole-map sweep: every override must target a key Astryx ships, hold no Latin outside {…}, and carry the same top-level ICU arguments as the en default — depth-aware extraction, since plural branch text ({result}) reads like an argument to a naive regex and would flag every zh string that drops an inapplicable plural. - Presence-helper comment reworded: the check reads the override map directly, so a deleted entry yields '' here; at runtime the same miss falls back to Astryx's shipped en catalog. Sweep red-validated both ways: a misspelled key fails membership, an English value fails the Latin check. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Ac5rv6WUKWtMZgQb5QPN16 --- .../ui/src/__tests__/markdown-body.test.ts | 64 +++++++++++++++++-- packages/ui/src/astryx-copy.ts | 24 +------ packages/ui/src/astryx-i18n.tsx | 8 --- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/packages/ui/src/__tests__/markdown-body.test.ts b/packages/ui/src/__tests__/markdown-body.test.ts index 1aa3adaea4..415965b9b1 100644 --- a/packages/ui/src/__tests__/markdown-body.test.ts +++ b/packages/ui/src/__tests__/markdown-body.test.ts @@ -1,4 +1,5 @@ import { strict as assert } from 'node:assert'; +import { createRequire } from 'node:module'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { it } from 'node:test'; @@ -230,11 +231,18 @@ it('localizes Astryx Markdown accessibility copy in Chinese', () => { function assertChineseAstryxOverrides(keys: readonly string[]) { const messages = astryxMessageOverrides('zh')?.zh ?? {}; for (const key of keys) { - // Assert presence first: a deleted key would coalesce to '', which holds no - // Latin letters and would satisfy the translation check on its own. + // Assert presence first: this reads the override map directly (no catalog + // resolution), so a deleted entry yields undefined → '' — which holds no + // Latin letters and would satisfy the translation check on its own. (At + // runtime the same missing entry falls back to Astryx's shipped en + // catalog, i.e. English in the UI.) const value = messages[key]; assert.ok(value, `missing override: ${key}`); - assert.doesNotMatch(value, /[A-Za-z]/, `untranslated: ${key}`); + assert.doesNotMatch( + value.replace(/\{[^}]*\}/g, ''), + /[A-Za-z]/, + `untranslated: ${key}`, + ); } } @@ -242,7 +250,55 @@ it('localizes the Astryx chat chrome adopted in #1795', () => { const messages = astryxMessageOverrides('zh')?.zh ?? {}; assert.equal(messages['@astryx.chatLayout.newMessages'], '跳到最新消息'); assert.equal(messages['@astryx.chatLayoutScrollButton.scrollToBottom'], '滚动到底部'); - assertChineseAstryxOverrides(['@astryx.chatSendButton.send', '@astryx.chat.status.sent']); + assertChineseAstryxOverrides(['@astryx.chatToolCalls.error', '@astryx.chat.status.sent']); +}); + +// Whole-map sweep: every override must target a key Astryx actually ships, +// carry the same ICU arguments as the en default (a renamed placeholder +// throws at format time), and hold no Latin outside {…} segments. The pinned +// tests above cover specific regressions; this keeps the other ~70 entries +// honest without naming them one by one. +it('every zh override is a real Astryx key, translated, with matching ICU args', () => { + const require = createRequire(import.meta.url); + const catalog: Record = require( + '@astryxdesign/core/locales/en.json', + ); + // Top-level ICU argument names only: inside `{count, plural, one {result}}` + // the `{result}` is branch text, not an argument — a naive regex would + // report it and flag every zh string that drops an inapplicable plural. + const icuArgs = (message: string) => { + const args = new Set(); + let depth = 0; + for (let i = 0; i < message.length; i++) { + if (message[i] === '{') { + if (depth === 0) { + const m = /^\{\s*([a-zA-Z0-9_]+)/.exec(message.slice(i)); + if (m?.[1]) args.add(m[1]); + } + depth++; + } else if (message[i] === '}') { + depth = Math.max(0, depth - 1); + } + } + return args; + }; + const messages = astryxMessageOverrides('zh')?.zh ?? {}; + assert.ok(Object.keys(messages).length > 0); + for (const [key, value] of Object.entries(messages)) { + const shipped = catalog[key]; + assert.ok(shipped, `override targets a key Astryx does not ship: ${key}`); + assert.ok(value, `empty override: ${key}`); + assert.doesNotMatch( + value.replace(/\{[^}]*\}/g, ''), + /[A-Za-z]/, + `untranslated: ${key} = ${value}`, + ); + assert.deepEqual( + icuArgs(value), + icuArgs(shipped.defaultMessage), + `ICU argument mismatch for ${key}: zh "${value}" vs en "${shipped.defaultMessage}"`, + ); + } }); it('localizes the Lightbox reached via useLightbox in chat-turn', () => { diff --git a/packages/ui/src/astryx-copy.ts b/packages/ui/src/astryx-copy.ts index 865f9b4dd3..fd0e4f1626 100644 --- a/packages/ui/src/astryx-copy.ts +++ b/packages/ui/src/astryx-copy.ts @@ -16,7 +16,6 @@ * config drifting against upstream. */ export interface AstryxCopy { - appShell: { mobileNavigation: string }; banner: { collapse: string; expand: string }; breadcrumbs: { label: string }; calendar: { @@ -45,8 +44,6 @@ export interface AstryxCopy { drawerExpand: string; newMessages: string; scrollToBottom: string; - send: string; - stop: string; toolCallsError: string; toolCallsGroupLabel: string; triggerSuggestions: string; @@ -82,20 +79,12 @@ export interface AstryxCopy { itemExpand: string; }; tabList: { label: string }; - table: { - label: string; - noData: string; - filterAll: string; - filterApply: string; - filterReset: string; - filterByColumn: string; - }; + table: { label: string }; thumbnail: { fallbackName: string; open: string; remove: string }; token: { remove: string }; } export const ASTRYX_COPY_ZH: AstryxCopy = { - appShell: { mobileNavigation: '移动端导航' }, banner: { collapse: '收起', expand: '展开' }, breadcrumbs: { label: '面包屑导航' }, calendar: { @@ -124,8 +113,6 @@ export const ASTRYX_COPY_ZH: AstryxCopy = { drawerExpand: '展开{label}', newMessages: '跳到最新消息', scrollToBottom: '滚动到底部', - send: '发送', - stop: '停止', toolCallsError: '错误:{message}', toolCallsGroupLabel: '{count} 次工具调用', triggerSuggestions: '建议', @@ -160,14 +147,7 @@ export const ASTRYX_COPY_ZH: AstryxCopy = { itemExpand: '展开{label}', }, tabList: { label: '标签页' }, - table: { - label: '表格', - noData: '暂无数据', - filterAll: '全部', - filterApply: '应用', - filterReset: '重置', - filterByColumn: '筛选{header}', - }, + table: { label: '表格' }, thumbnail: { fallbackName: '缩略图', open: '打开{accessibleName}', remove: '移除{accessibleName}' }, token: { remove: '移除{label}' }, }; diff --git a/packages/ui/src/astryx-i18n.tsx b/packages/ui/src/astryx-i18n.tsx index d4c6e9b667..34c4611e78 100644 --- a/packages/ui/src/astryx-i18n.tsx +++ b/packages/ui/src/astryx-i18n.tsx @@ -96,8 +96,6 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined '@astryx.chatComposerDrawer.expand': astryx.chat.drawerExpand, '@astryx.chatLayout.newMessages': astryx.chat.newMessages, '@astryx.chatLayoutScrollButton.scrollToBottom': astryx.chat.scrollToBottom, - '@astryx.chatSendButton.send': astryx.chat.send, - '@astryx.chatSendButton.stop': astryx.chat.stop, '@astryx.chatToolCalls.error': astryx.chat.toolCallsError, '@astryx.chatToolCalls.groupLabel': astryx.chat.toolCallsGroupLabel, '@astryx.chatTriggerMenu.suggestions': astryx.chat.triggerSuggestions, @@ -154,7 +152,6 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined '@astryx.lightbox.next': astryx.lightbox.next, // Shell chrome: side nav, tabs, banners, breadcrumbs, resize handles. - '@astryx.appShell.mobileNavigation': astryx.appShell.mobileNavigation, '@astryx.banner.collapse': astryx.banner.collapse, '@astryx.banner.expand': astryx.banner.expand, '@astryx.banner.dismiss': shared.primitives.close, @@ -169,11 +166,6 @@ export function astryxMessageOverrides(locale: UiLocale): Overrides | undefined // Table (usage settings) and the chat transcript's attachment chrome. '@astryx.table.label': astryx.table.label, - '@astryx.table.noData': astryx.table.noData, - '@astryx.table.filter.allPlaceholder': astryx.table.filterAll, - '@astryx.table.filter.apply': astryx.table.filterApply, - '@astryx.table.filter.reset': astryx.table.filterReset, - '@astryx.tableFiltering.filterByColumn': astryx.table.filterByColumn, '@astryx.thumbnail.fallbackName': astryx.thumbnail.fallbackName, '@astryx.thumbnail.open': astryx.thumbnail.open, '@astryx.thumbnail.remove': astryx.thumbnail.remove,