Back to Cherry Studio

Text Translation

docs/references/ai/translation.md

2.0.104.6 KB
Original Source

Text Translation

This document covers one-shot text translation through translate.open. PDF translation uses the separate translate.pdf.* routes and PdfTranslationService.

Active flow

text
MessageMenuBar
  -> MessageListActions.translateMessage
  -> homeMessageListAdapter.translateMessage
       |-> ChatWrite.editMessage(data-translation)
       `-> translateText
            -> ipcApi.request('translate.open', { streamId, text, targetLangCode })
            -> translateHandlers['translate.open']
            -> translateService.open
            -> AiStreamManager.streamPrompt
            -> WebContentsListener
            -> ai.stream.chunk / done / error

Other renderer surfaces, including TranslatePage and selection translation, reach the same translateText helper through useTranslate. They consume the returned text locally instead of attaching it to a chat message.

Ownership

The responsibilities deliberately split at the renderer/Main boundary:

OwnerResponsibility
Renderer callerDecide what the translated text means and whether to persist it
translateTextGenerate a stream ID, subscribe before opening, accumulate chunks, and bridge abort
translate.open handlerValidate the managed-window sender and delegate to the service
translateServiceResolve the configured model and language, build the prompt, and open the prompt stream
AiStreamManagerRun the prompt stream and deliver chunks through WebContentsListener

translateService is a direct-import singleton because it owns no long-lived resource or persistent side effect. The lifecycle-owned IpcApiService owns the persistent IPC registration.

IPC contract

The renderer sends exactly three fields:

ts
ipcApi.request('translate.open', {
  streamId,
  text,
  targetLangCode
})
  • streamId is renderer-generated and must start with translate:. The namespace prevents collisions with real chat topic IDs when abort uses ai.stream.abort({ topicId: streamId }).
  • targetLangCode must be a concrete configured language, not unknown.
  • The renderer subscribes to ai.stream.chunk, ai.stream.done, and ai.stream.error before it calls translate.open, because Main starts the stream synchronously.

The route has no messageId or sourceLangCode. Main therefore has no message target and cannot persist chat data from this route.

Home message persistence

Home chat owns the message projection through its existing chat write boundary:

  1. homeMessageListAdapter.translateMessage aborts any older translation for the same message.
  2. It writes an empty data-translation part so the loading UI has a committed target.
  3. Each accumulated response replaces that part through ChatWrite.editMessage; updates are serialized so a slower write cannot overwrite a later chunk.
  4. Completion waits for pending writes. Failure or abort removes the loading part when that controller still owns the translation.

This keeps message persistence with the same owner as every other message edit. Callers without a message target keep their result locally. translate.open does not write translate_history rows.

Why there is no translation overlay store

An older message-bound path mounted useTranslateMessage, wrote streamed text into TranslationOverlayContext, passed messageId to translate.open, and attached a Main-side TranslationBackend. The Home message menu was later moved to homeMessageListAdapter.translateMessage; no production caller of useTranslateMessage remained.

That orphaned branch was removed instead of adding a keyed external store or a Cache key. There is no independent temporary-state owner to coordinate:

  • the active stream state is local to the Home adapter;
  • the user-visible translation is message business data written through ChatWrite;
  • other translation callers own their returned text locally.

Reintroducing an overlay or Cache-backed store requires a concrete production consumer and a lifecycle that the existing caller-owned flow cannot satisfy.

Validation

  • src/renderer/utils/translate/__tests__/translateText.test.ts covers stream IDs, chunk accumulation, terminal events, errors, and abort.
  • src/renderer/pages/home/messages/__tests__/homeMessageListAdapter.test.tsx covers keeping the translation active until its final write settles.
  • src/main/services/translate/__tests__/translateService.test.ts covers model/prompt resolution, request validation, and stream dispatch.
  • src/main/ipc/handlers/__tests__/translate.test.ts covers sender resolution and handler delegation.