Skip to content

fix(i18n): make resume-park and connection-error copy locale-aware #4489

Description

@ying-hua

What happened

Two shared copy modules predate the locale authority and expose no UiLocale parameter. One is a user-visible defect; the other is a stale boundary that will produce one.

1. resumeParkToastCopy is Chinese-only and is rendered directly

packages/ui/src/runtime-resume-copy.ts holds 29 hardcoded Chinese reason strings plus 4 inline title/fallback strings, and its entry point takes no locale:

export function resumeParkToastCopy(reasons: readonly string[]): ResumeParkToastCopy

apps/desktop/src/renderer/use-shell-resume.ts calls it and renders the result unchanged:

  • line 67 — const parkCopy = resumeParkToastCopy(result.rejectionReasons);
  • line 72 — toastApi.error(parkCopy.title, parkCopy.description, undefined, { sessionId });
  • lines 68–71 — the same Chinese description is stored in resumeParkDescriptionBySession, which apps/desktop/src/renderer/app-shell.tsx:3149 renders as detail:

So an English-locale Desktop user who resumes a parked session sees 暂时无法继续这一轮 as the toast title, a Chinese description, and the same Chinese text again in the inline detail.

The locale is already in scope in that exact function. use-shell-resume.ts:56 destructures uiLocale, and line 87 passes it to localizedShellErrorMessage. Within one try block:

  • the success path (line 79) uses shellCopy.resumeStartedTitle — localized
  • the throw path (lines 83–88) uses localizedShellErrorMessage(error, ..., uiLocale) — localized
  • the park path (lines 67–72) — not localized

Only the park branch skips the locale it is holding.

2. connection-error-copy.ts is a superseded copy table with a stale contract

packages/core/src/connection-error-copy.ts returns hardcoded Chinese from REASON_FIX_COPY (line 48) and GENERIC_FIX_COPY (line 39) through describeChatConfigurationReason (line 77), also with no locale parameter.

This is not user-visible today, but the module's stated contract no longer matches the code. Its docstring says:

This module is the canonical parser and copy table for both CLI and desktop; surfaces adapt their local event shape here instead of duplicating the rules.

The parser half is still canonical. The copy half is not:

  • Desktop resolves its own localized table — apps/desktop/src/renderer/model-connection-errors.ts:49-53 reads getDesktopConversationCopy(locale).model.configurationReason[reason], keyed off the parsed reason token. It never reads describeChatConfigurationReason.
  • The only remaining caller is packages/runtime-host/src/server/execution-composition.ts:310, which embeds the Chinese sentence into a NO_REAL_CONNECTION:fake_backend: <message> error string that every consumer then discards — consumers parse the reason token and look up their own copy.
  • packages/cli/src/runtime-host-cli-context.ts:306 hand-rolls the English equivalent of the same code: NO_REAL_CONNECTION:missing_default_connection: Runtime Host has no default model connection.

So the same error code carries a different language depending on which producer constructed it, and the module that claims to own that copy is no longer the one supplying it.

How to reproduce

For (1), on main:

  1. Settings → Preferences → set the language to English.
  2. Put a session into a parked resume state — any run whose resumeLatest returns disposition: 'park' with at least one rejectionReasons entry (for example a run left with dangling tool state).
  3. Click resume on that session.
  4. The toast title and description render in Chinese, and the inline detail under the task shows the same Chinese text.

The code path is deterministic rather than timing-dependent: use-shell-resume.ts:67 has no locale argument to pass, so no locale can influence the result.

For (2) there is no user-visible reproduction — it is a boundary/ownership defect, filed here because it is the same root cause found in the same review.

Environment

  • Commit: 98fc50508 (main, 2026-09-01)
  • Maka: 0.1.11
  • Surface: Desktop (1); Runtime Host and CLI (2)
  • Locale: en

Desired outcome

  • resumeParkToastCopy accepts a UiLocale and resolves its strings through a catalog; use-shell-resume.ts passes the uiLocale it already destructures. English mode shows no Chinese in the park toast or the inline detail.
  • connection-error-copy.ts either becomes locale-aware or drops its copy table and keeps only the parser. Either way its docstring should state what it actually owns, and the runtime-host and CLI producers should stop disagreeing about language for the same code.
  • Reason tokens, error codes, logs, and protocol values stay locale-independent. Only presentation changes.
  • A new reason cannot be added to either module without an en entry.

Notes on approach

packages/core/src/ui-locale.ts on main now ships UiMessageCatalog<T>, defineUiMessageCatalog, resolveUiMessageCatalog, and formatUiMessage, added by #3990 and currently used only inside packages/cli. Because UiMessageCatalog requires en and treats every other locale as an optional DeepPartial, it fits these two modules better than the strict UiCatalog<T> = Record<UiLocale, T> and avoids forcing a repo-wide atomic change.

I have no strong preference between that and a plain UiCatalog; deferring to whichever the maintainers consider canonical for packages/ui and packages/core right now.

Relationship to existing issues


AI disclosure: drafted with Claude Code. Every file path, line number, and quoted string above was read from apache/maka@98fc50508 and verified before filing.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions