diff --git a/apps/desktop/src/main/__tests__/attachment-preflight.test.ts b/apps/desktop/src/main/__tests__/attachment-preflight.test.ts index d38c2c008c..f7b7aee817 100644 --- a/apps/desktop/src/main/__tests__/attachment-preflight.test.ts +++ b/apps/desktop/src/main/__tests__/attachment-preflight.test.ts @@ -29,19 +29,19 @@ describe('attachment preflight (before session create)', () => { size: 100, source: { type: 'file' as const, file: { size: 100 } }, })); - assert.throws(() => preflightAttachmentItems(items), /8/); + assert.throws(() => preflightAttachmentItems(items, 'zh'), /8/); }); test('rejects an oversized File so no empty session is created', () => { assert.throws( - () => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'file', file: { size: CAP + 1 } } }]), + () => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'file', file: { size: CAP + 1 } } }], 'zh'), /50MB/, ); }); test('rejects an oversized approval-token attachment by pending size', () => { assert.throws( - () => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'approval', approvalId: 'a1' } }]), + () => preflightAttachmentItems([{ size: CAP + 1, source: { type: 'approval', approvalId: 'a1' } }], 'zh'), /50MB/, ); }); @@ -52,9 +52,17 @@ describe('attachment preflight (before session create)', () => { preflightAttachmentItems([ { size: 10, source: { type: 'approval', approvalId: 'dup' } }, { size: 10, source: { type: 'approval', approvalId: 'dup' } }, - ]), + ], 'zh'), /重复/, ); + assert.throws( + () => + preflightAttachmentItems([ + { size: 10, source: { type: 'approval', approvalId: 'dup' } }, + { size: 10, source: { type: 'approval', approvalId: 'dup' } }, + ], 'en'), + /already added/, + ); }); test('passes approval tokens and files under the cap', () => { @@ -62,7 +70,7 @@ describe('attachment preflight (before session create)', () => { preflightAttachmentItems([ { size: 100, source: { type: 'approval', approvalId: 'a1' } }, { size: 100, source: { type: 'file', file: { size: 100 } } }, - ]), + ], 'zh'), ); }); }); \ No newline at end of file diff --git a/apps/desktop/src/main/__tests__/conversation-markdown.test.ts b/apps/desktop/src/main/__tests__/conversation-markdown.test.ts index cd8cafacec..7d56f293d2 100644 --- a/apps/desktop/src/main/__tests__/conversation-markdown.test.ts +++ b/apps/desktop/src/main/__tests__/conversation-markdown.test.ts @@ -58,10 +58,11 @@ describe('renderConversationMarkdown', () => { modelId: 'fake', }, ]; - const md = renderConversationMarkdown('skill session', messages); + const md = renderConversationMarkdown('skill session', messages, 'zh'); assert.match(md, /## 你/); assert.ok(md.includes(typed), 'export shows the typed prompt'); assert.ok(!md.includes(' { }), ], '', + 'zh', ); const keys = options.map(([key]) => key); assert.ok( @@ -198,4 +199,9 @@ describe('model catalog picker helpers', () => { `unsupported Codex model was offered: ${JSON.stringify(keys)}`, ); }); + + it('labels a saved-but-unavailable selection in the UI locale', () => { + const [, label] = buildCatalogDailyReviewModelOptions([], 'codex::gone', 'en').at(-1)!; + assert.equal(label, 'gone · codex · Currently unavailable'); + }); }); diff --git a/apps/desktop/src/main/__tests__/session-error-presentation.test.ts b/apps/desktop/src/main/__tests__/session-error-presentation.test.ts index 750d1d3cda..7ec008d9f6 100644 --- a/apps/desktop/src/main/__tests__/session-error-presentation.test.ts +++ b/apps/desktop/src/main/__tests__/session-error-presentation.test.ts @@ -26,12 +26,13 @@ import { describeTurnErrorClass } from '../../renderer/session-status-presentati describe('provider capacity presentation', () => { it('uses capacity-specific copy instead of the unknown error fallback', () => { - assert.match(describeSessionErrorReason('provider_capacity') ?? '', /满载/); - assert.match(describeTurnErrorClass('provider_capacity'), /满载/); + assert.match(describeSessionErrorReason('provider_capacity', 'zh') ?? '', /满载/); + assert.match(describeSessionErrorReason('provider_capacity', 'en') ?? '', /at capacity/); + assert.match(describeTurnErrorClass('provider_capacity', 'zh'), /满载/); }); it('does not recommend an immediate direct retry', () => { - const label = describeTurnErrorClass('provider_capacity'); + const label = describeTurnErrorClass('provider_capacity', 'zh'); assert.match(label, /等几分钟|换一个模型/); assert.doesNotMatch(label, /直接重试/); }); diff --git a/apps/desktop/src/renderer/attachment-preflight.ts b/apps/desktop/src/renderer/attachment-preflight.ts index 0af114c7a9..33891c2fdc 100644 --- a/apps/desktop/src/renderer/attachment-preflight.ts +++ b/apps/desktop/src/renderer/attachment-preflight.ts @@ -38,7 +38,7 @@ type PreflightItem = { * File blobs are sized by the browser File object; approval-token attachments * are sized by the pending size stamped at pick time (main re-stats). */ -export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale = 'zh'): void { +export function preflightAttachmentItems(items: readonly PreflightItem[], locale: UiLocale): void { const copy = getDesktopConversationCopy(locale).attachments; if (items.length > MAX_ATTACHMENT_COUNT) throw new Error(copy.tooMany); const seen = new Set(); diff --git a/apps/desktop/src/renderer/conversation-markdown.ts b/apps/desktop/src/renderer/conversation-markdown.ts index 526f41d701..d89d291fd7 100644 --- a/apps/desktop/src/renderer/conversation-markdown.ts +++ b/apps/desktop/src/renderer/conversation-markdown.ts @@ -43,7 +43,7 @@ import { getShellRemainingCopy } from './locales/shell-remaining-copy.js'; * export that the user is going to paste somewhere public. * - **user text** left untouched (the user typed it, they own it). */ -export function renderConversationMarkdown(sessionName: string, messages: StoredMessage[], locale: UiLocale = 'zh'): string { +export function renderConversationMarkdown(sessionName: string, messages: StoredMessage[], locale: UiLocale): string { const copy = getShellRemainingCopy(locale).conversationExport; const lines: string[] = []; lines.push(`# ${sessionName}`); diff --git a/apps/desktop/src/renderer/derive-turn-lineage-badges.ts b/apps/desktop/src/renderer/derive-turn-lineage-badges.ts index f6aa1d8373..c90ab67484 100644 --- a/apps/desktop/src/renderer/derive-turn-lineage-badges.ts +++ b/apps/desktop/src/renderer/derive-turn-lineage-badges.ts @@ -46,11 +46,11 @@ export interface TurnLineageBadgeInput { regeneratedToTurnId?: string; /** True when the target turn id still exists in the materialized view. */ existsTurn(turnId: string): boolean; - locale?: UiLocale; + locale: UiLocale; } export function deriveTurnLineageBadges(input: TurnLineageBadgeInput): TurnLineageBadge[] { - const copy = getDesktopConversationCopy(input.locale ?? 'zh').lineage; + const copy = getDesktopConversationCopy(input.locale).lineage; const badges: TurnLineageBadge[] = []; const forwardFrom = input.regeneratedFromTurnId ?? input.retriedFromTurnId; diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts index 92dc3a9231..b0ab78e475 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts +++ b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts @@ -28,7 +28,7 @@ import { cleanErrorMessage } from '../../application/contracts/connection-error- export type CredentialPresenceStatus = boolean | 'loading' | 'error'; -export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale = 'zh'): string { +export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale): string { const shared = getProviderSettingsCopy(locale).shared; // Electron wraps ipcMain.handle rejections as "Error invoking remote method // '': Error: ". Classify the original message, not the @@ -64,7 +64,7 @@ export interface ConnectionTestTroubleshootingCopy { export function connectionTestFailureFallback( result: ConnectionTestResult, copy: ConnectionTestTroubleshootingCopy, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const shared = getProviderSettingsCopy(locale).shared; if (result.statusCode === 429) return shared.rateLimit; @@ -82,7 +82,7 @@ export function connectionTestFailureFallback( export function connectionTestFailureMessage( result: ConnectionTestResult, copy: ConnectionTestTroubleshootingCopy, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const fallback = connectionTestFailureFallback(result, copy, locale); if (!result.errorMessage) return fallback; @@ -91,7 +91,7 @@ export function connectionTestFailureMessage( : generalizedErrorMessage(new Error(result.errorMessage), fallback); } -export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale = 'zh'): string | undefined { +export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale): string | undefined { if (!message) return undefined; const trimmed = message.trim(); if (!trimmed) return undefined; @@ -105,6 +105,6 @@ export function connectionLastTestMessageDisplay(message: string | undefined, lo return classified || copy.statusUnavailable; } -export function categoryLabel(category: ProviderCategory, locale: UiLocale = 'zh'): string { +export function categoryLabel(category: ProviderCategory, locale: UiLocale): string { return getProviderSettingsCopy(locale).shared.categories[category]; } diff --git a/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts b/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts index 8fa85cf5a6..146d1bcd4f 100644 --- a/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts +++ b/apps/desktop/src/renderer/features/session-navigation/model/session-project-grouping.ts @@ -28,7 +28,7 @@ const UNGROUPED_KEY = '__ungrouped__'; export function deriveProjectGroups( sessions: ReadonlyArray, projects: ReadonlyArray, - locale: UiLocale = 'zh', + locale: UiLocale, ): SessionHistoryGroup[] { const sessionsByProject = new Map(); const canonicalProjectIds = new Map(); diff --git a/apps/desktop/src/renderer/model-catalog-choices.ts b/apps/desktop/src/renderer/model-catalog-choices.ts index d1d8b09a46..aafd68e3b0 100644 --- a/apps/desktop/src/renderer/model-catalog-choices.ts +++ b/apps/desktop/src/renderer/model-catalog-choices.ts @@ -48,7 +48,7 @@ export function buildCatalogRecommendedDefaultModel(providerType: ProviderType): export function buildCatalogDailyReviewModelOptions( connections: readonly (LlmConnection & HostResolvedConnectionCatalog)[], currentModelKey: string, - locale: UiLocale = 'zh', + locale: UiLocale, ): Array { const current = parseDailyReviewModelKey(currentModelKey); const candidates: Array<{ key: string; label: string; safeSourceLabel: string }> = []; diff --git a/apps/desktop/src/renderer/model-connection-errors.ts b/apps/desktop/src/renderer/model-connection-errors.ts index fe3fdf5568..d399fcaa41 100644 --- a/apps/desktop/src/renderer/model-connection-errors.ts +++ b/apps/desktop/src/renderer/model-connection-errors.ts @@ -46,7 +46,7 @@ export function noRealConnectionReasonFromEvent(event: Extract, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { if (isNoRealConnectionEvent(event)) { return noRealConnectionSetupDescription(noRealConnectionReasonFromEvent(event), locale); @@ -69,7 +69,7 @@ export function sessionEventErrorMessage( export function modelSetupToastCopy( reason: string | undefined, fallback: string, - locale: UiLocale = 'zh', + locale: UiLocale, ): { title: string; description: string } { const copy = getDesktopConversationCopy(locale).model; if (reason === 'connection_missing') { diff --git a/apps/desktop/src/renderer/session-error-presentation.ts b/apps/desktop/src/renderer/session-error-presentation.ts index f3bccf1e7b..b1c5fa85a2 100644 --- a/apps/desktop/src/renderer/session-error-presentation.ts +++ b/apps/desktop/src/renderer/session-error-presentation.ts @@ -25,7 +25,7 @@ import { getDesktopConversationCopy } from './locales/conversation-copy.js'; * runtime. Unknown reasons intentionally return undefined so callers can use * their existing safe fallback instead of displaying raw provider text. */ -export function describeSessionErrorReason(reason: string | undefined, locale: UiLocale = 'zh'): string | undefined { +export function describeSessionErrorReason(reason: string | undefined, locale: UiLocale): string | undefined { const copy = getDesktopConversationCopy(locale).turnError; switch (reason?.toLowerCase()) { case 'context_overflow': diff --git a/apps/desktop/src/renderer/session-status-presentation.ts b/apps/desktop/src/renderer/session-status-presentation.ts index c039d73e30..36b7993054 100644 --- a/apps/desktop/src/renderer/session-status-presentation.ts +++ b/apps/desktop/src/renderer/session-status-presentation.ts @@ -97,7 +97,7 @@ export function normalizeSessionSummaryForDisplay(sess * the UI; they just fall through to the catch-all until the mapping * is extended. */ -export function describeTurnErrorClass(errorClass: string | undefined, locale: UiLocale = 'zh'): string { +export function describeTurnErrorClass(errorClass: string | undefined, locale: UiLocale): string { const copy = getDesktopConversationCopy(locale).turnError; if (!errorClass) return copy.unknown; const reasonDescription = describeSessionErrorReason(errorClass, locale); @@ -169,7 +169,7 @@ export interface FailedTurnExecutionState { */ export function describeFailedTurnExecutionState( state: FailedTurnExecutionState, - locale: UiLocale = 'zh', + locale: UiLocale, ): string | undefined { const copy = getDesktopConversationCopy(locale).turnError.executionState; if (state.erroredToolCount > 0) return copy.erroredTool; diff --git a/apps/desktop/src/renderer/settings/bot-chat-shared.tsx b/apps/desktop/src/renderer/settings/bot-chat-shared.tsx index 101d069aa7..5ec2b5dee9 100644 --- a/apps/desktop/src/renderer/settings/bot-chat-shared.tsx +++ b/apps/desktop/src/renderer/settings/bot-chat-shared.tsx @@ -60,7 +60,7 @@ export const BOT_LABELS: Record; - locale?: UiLocale; + locale: UiLocale; } /** @@ -113,7 +113,7 @@ export interface TurnFooterContext { */ export function deriveTurnFooterActions(input: TurnFooterContext): TurnFooterAction[] { const { status, hasContent, alreadyRegenerated, pendingActions, metaSummary } = input; - const copyText = getDesktopConversationCopy(input.locale ?? 'zh').footer; + const copyText = getDesktopConversationCopy(input.locale).footer; const actionLabel = copyText.labels; const isPending = (id: TurnFooterActionId) => pendingActions?.has(id) ?? false; const PENDING_TOOLTIP = copyText.pending; diff --git a/packages/core/src/relative-time.ts b/packages/core/src/relative-time.ts index 5ce6f475bd..d31e1f0f70 100644 --- a/packages/core/src/relative-time.ts +++ b/packages/core/src/relative-time.ts @@ -93,7 +93,7 @@ function getAbsoluteFormat(uiLocale: UiLocale): Intl.DateTimeFormat { * reading a relative label falls back to and a tooltip shows; `@maka/ui` had * its own uncached copy of the same `Intl` options until this became public. */ -export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh'): string { +export function formatAbsoluteTimestamp(ts: number, locale: UiLocale): string { return getAbsoluteFormat(locale).format(new Date(ts)); } @@ -105,7 +105,7 @@ export function formatAbsoluteTimestamp(ts: number, locale: UiLocale = 'zh'): st export function formatRelativeTimestamp( ts: number, now: number = Date.now(), - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const diffMs = relativeAgeMs(ts, now); if (diffMs < JUST_NOW_MS) { @@ -158,7 +158,7 @@ function getCompactFormats(uiLocale: UiLocale): { export function formatCompactTimestamp( ts: number, now: number = Date.now(), - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const diffMs = relativeAgeMs(ts, now); if (diffMs <= RELATIVE_HORIZON_MS) return formatRelativeTimestamp(ts, now, locale); @@ -178,7 +178,7 @@ export function formatCompactTimestamp( export function formatSidebarTimestamp( ts: number, now: number = Date.now(), - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const diffMs = relativeAgeMs(ts, now); if (diffMs < JUST_NOW_MS) return JUST_NOW[locale]; diff --git a/packages/core/src/tool-quiet-preview.ts b/packages/core/src/tool-quiet-preview.ts index e2adb0d7b6..7d4247e8fe 100644 --- a/packages/core/src/tool-quiet-preview.ts +++ b/packages/core/src/tool-quiet-preview.ts @@ -230,7 +230,7 @@ export interface ToolInvocationInput { */ export function formatToolInvocationLine( item: ToolInvocationInput, - locale: UiLocale = 'zh', + locale: UiLocale, ): string | undefined { const s = strings(locale); const args = asRecord(item.args); @@ -518,7 +518,7 @@ export interface QuietPreview { * Primary list/text fields become the main body; remaining fields (error, ok, * truncated, …) are appended so diagnostics cannot vanish. */ -export function formatQuietJsonValue(value: unknown, locale: UiLocale = 'zh'): QuietPreview { +export function formatQuietJsonValue(value: unknown, locale: UiLocale): QuietPreview { const s = strings(locale); if (value === null || value === undefined) { return { body: s.empty }; @@ -677,7 +677,7 @@ function formatArrayAsBody(values: unknown[], locale: UiLocale): string { export function formatAsKeyValueLines( record: Record, depth = 0, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const s = strings(locale); if (depth > 3) return redactSecrets(String(record)); diff --git a/packages/ui/src/artifact-preview-registry.ts b/packages/ui/src/artifact-preview-registry.ts index def9198076..89158dff5c 100644 --- a/packages/ui/src/artifact-preview-registry.ts +++ b/packages/ui/src/artifact-preview-registry.ts @@ -81,7 +81,7 @@ function exceedsImagePayloadCap(base64: string): boolean { return base64.length > IMAGE_PAYLOAD_MAX_BASE64_LENGTH; } -export function formatPreviewSize(sizeBytes: number | undefined, locale: UiLocale = 'zh'): string { +export function formatPreviewSize(sizeBytes: number | undefined, locale: UiLocale): string { if (sizeBytes === undefined || sizeBytes < 0 || !Number.isFinite(sizeBytes)) return getSharedUiCopy(locale).artifact.unknownSize; if (sizeBytes < 1024) return `${sizeBytes} B`; if (sizeBytes < 1024 * 1024) return `${(sizeBytes / 1024).toFixed(1)} KB`; diff --git a/packages/ui/src/chat-model-helpers.ts b/packages/ui/src/chat-model-helpers.ts index 385dc167da..3edc61911f 100644 --- a/packages/ui/src/chat-model-helpers.ts +++ b/packages/ui/src/chat-model-helpers.ts @@ -49,7 +49,7 @@ export type { ChatModelChoice } from '@maka/core/chat-model-choice'; export function modelChoiceDescription( choice: Pick, - locale: UiLocale = 'zh', + locale: UiLocale, ): string | undefined { const description = choice.description?.trim(); const knowledge = choice.knowledgeCutoff?.trim(); @@ -85,7 +85,7 @@ export interface ModelMenuGroup { * account email `connection.name` carries for `claude-subscription` / * `openai-codex`. */ -export function modelMenuGroups(choices: ChatModelChoice[], locale: UiLocale = 'zh'): ModelMenuGroup[] { +export function modelMenuGroups(choices: ChatModelChoice[], locale: UiLocale): ModelMenuGroup[] { const copy = getSharedUiCopy(locale).providers; const localizedLabels: Partial> = { 'MiniMax-cn': copy.minimaxChina, diff --git a/packages/ui/src/session-status-presentation.ts b/packages/ui/src/session-status-presentation.ts index a2f5c5cf9a..9037b7db3a 100644 --- a/packages/ui/src/session-status-presentation.ts +++ b/packages/ui/src/session-status-presentation.ts @@ -66,7 +66,7 @@ const STATUS_SEMANTIC: Record = { export function presentSessionStatus( status: SessionStatus, - locale: UiLocale = 'zh', + locale: UiLocale, ): SessionStatusPresentation { const semantic = STATUS_SEMANTIC[status]; return { @@ -84,7 +84,7 @@ export function presentSessionStatus( */ export function describeBlockedReason( reason: SessionBlockedReason | undefined, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const copy = getConversationCopy(locale).sessions.blockedReason; return reason ? copy[reason] : copy.unknown; diff --git a/packages/ui/stories/model-picker.stories.tsx b/packages/ui/stories/model-picker.stories.tsx index b8cd7f7fd9..7774685f20 100644 --- a/packages/ui/stories/model-picker.stories.tsx +++ b/packages/ui/stories/model-picker.stories.tsx @@ -354,7 +354,7 @@ export const SavingDefaultModel: Story = { render: () => (