feat(web): localize the subagent catalog and read-only composer copy
The catalog action (diagnostics, relative times, loading/error/retry,
mode and activity labels, branch toggles, descendant counts, tree aria)
and the read-only composer were hardcoded to Simplified Chinese, so an
English-locale session rendered mixed-language UI. Register a `subagent`
locale namespace (zh source of truth + en dictionary), declare it on both
slot registrations, thread the locale `t` seat through the components, and
mount the locale service in the plugin specs.
The UI spec's zh assertions now run against the real dictionary through a
`t` stub that interpolates `{name}` params exactly like the locale
service.
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
import type { ComposerChainProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import type { ClientSessionContext, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import { apply as applyLocale } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import {
|
||||
SubagentCatalogAction, type SubagentCatalogInjected,
|
||||
} from '../src/client/SubagentCatalogAction.tsx'
|
||||
@@ -85,6 +86,7 @@ async function fullBench(sessions: SessionSummary[]) {
|
||||
ctx.provide('slash', { registerSource: (src: SlashSource) => { captured = src; return () => {} } })
|
||||
ctx.provide('sessions', face)
|
||||
await provideSlotFaces(ctx)
|
||||
await ctx.plugin({ inject: ['slots'], apply: applyLocale }).await()
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
return { source: captured!, face, ctx }
|
||||
}
|
||||
@@ -111,7 +113,7 @@ const req = (query: string) =>
|
||||
|
||||
describe('apply', () => {
|
||||
it('declares the services it binds', () => {
|
||||
expect(inject).toEqual(['slash', 'sessions', 'conversation', 'slots'])
|
||||
expect(inject).toEqual(['slash', 'sessions', 'conversation', 'slots', 'locale'])
|
||||
})
|
||||
|
||||
it('registers the "@" subagent source; disposal frees the name (HMR safety)', async () => {
|
||||
@@ -119,6 +121,7 @@ describe('apply', () => {
|
||||
await ctx.plugin(SlashService).await()
|
||||
ctx.provide('sessions', sessionsWith(FAMILY))
|
||||
await provideSlotFaces(ctx)
|
||||
await ctx.plugin({ inject: ['slots'], apply: applyLocale }).await()
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
const slash = ctx.get('slash') as SlashService
|
||||
|
||||
@@ -7,7 +7,10 @@ import type {
|
||||
import {
|
||||
SubagentCatalogAction, type SubagentCatalogActionProps,
|
||||
} from '../src/client/SubagentCatalogAction.tsx'
|
||||
import { SubagentReadOnlyComposer } from '../src/client/SubagentReadOnlyComposer.tsx'
|
||||
import {
|
||||
SubagentReadOnlyComposer, type SubagentReadOnlyComposerProps,
|
||||
} from '../src/client/SubagentReadOnlyComposer.tsx'
|
||||
import { zh, type SubagentKey } from '../src/client/locales.ts'
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
@@ -63,12 +66,22 @@ function props(
|
||||
function useSessions<T>(select: (snapshot: SessionListState) => T): T {
|
||||
return select(state)
|
||||
}
|
||||
// The zh dictionary is the source of truth for this spec's assertions:
|
||||
// the stub interpolates `{name}` params like the locale service does.
|
||||
const t = ((key: SubagentKey, params?: Record<string, unknown>): string => {
|
||||
let text = zh[key]
|
||||
for (const [name, value] of Object.entries(params ?? {})) {
|
||||
text = text.replaceAll(`{${name}}`, String(value))
|
||||
}
|
||||
return text
|
||||
}) as SubagentCatalogActionProps['t']
|
||||
return {
|
||||
sessionId: PARENT,
|
||||
useSessions,
|
||||
openChild: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
setCatalogOpen: vi.fn(),
|
||||
t,
|
||||
} as unknown as SubagentCatalogActionProps
|
||||
}
|
||||
|
||||
@@ -453,13 +466,16 @@ describe('SubagentCatalogAction', () => {
|
||||
})
|
||||
|
||||
describe('SubagentReadOnlyComposer', () => {
|
||||
// The zh dictionary is the source of truth for this spec's assertions.
|
||||
const t = ((key: SubagentKey): string => zh[key]) as SubagentReadOnlyComposerProps['t']
|
||||
|
||||
it('explains the exact missing-parent recovery path', () => {
|
||||
render(<SubagentReadOnlyComposer matched={{ reason: 'parent-unavailable' }} />)
|
||||
render(<SubagentReadOnlyComposer matched={{ reason: 'parent-unavailable' }} t={t} />)
|
||||
expect(screen.getByRole('status').textContent).toContain('父会话当前不在线')
|
||||
})
|
||||
|
||||
it('explains that one-shot histories never accept follow-ups', () => {
|
||||
render(<SubagentReadOnlyComposer matched={{ reason: 'one-shot' }} />)
|
||||
render(<SubagentReadOnlyComposer matched={{ reason: 'one-shot' }} t={t} />)
|
||||
expect(screen.getByRole('status').textContent).toContain('一次性任务不支持后续消息')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user