Skip to content

Add on-device per-message translation#88

Draft
rexbron wants to merge 1 commit into
subpop:mainfrom
rexbron:message-translation
Draft

Add on-device per-message translation#88
rexbron wants to merge 1 commit into
subpop:mainfrom
rexbron:message-translation

Conversation

@rexbron
Copy link
Copy Markdown
Contributor

@rexbron rexbron commented May 7, 2026

Summary

User-initiated, on-device translation for received messages using Apple's Translation framework — no server round-trip, no third-party API.

  • Right-click or long-press a message → Translate runs the message body through TranslationSession and swaps the bubble to the translated text.
  • Right-click → Show Original reverts.
  • Translated bubbles get a translate-glyph badge; hovering surfaces the original text.
  • The system download prompt for missing language pairs is reached via SwiftUI's .translationTask (the only public entry point that triggers it).
  • Up to 3 translations run in parallel through a small slot pool — clicking Translate on several rows back-to-back no longer queues serially.
  • Source language is auto-detected with NLLanguageRecognizer; messages already in the user's current locale are skipped silently (no badge, no menu item).

Implementation notes

  • New RelayKit/Translation/MessageTranslator.swift handles source detection and the readable-language heuristic. It deliberately does not drive translation directly — the framework only prompts for downloads through .translationTask.
  • TimelineViewModel exposes a FIFO queue (pendingTranslationQueueVersion + claimNextTranslation() + runTranslation(for:translate:)). Atomicity comes from @MainActor isolation.
  • TimelineView renders 3 invisible TranslationSlot subviews; each owns its own Configuration @State and .translationTask, so slots are independent.
  • TimelineRowView's Equatable was updated to include row.translation and translationsVersion, otherwise the NSTableView cell-recycling path would skip body re-evaluation after a translation completed.
  • Translation is presentation-only — copy / reply / quote / edit all continue to operate on the original body. Not persisted across launches.

Test plan

  • Right-click a non-locale message → Translate appears; clicking it badges the row and swaps the bubble text.
  • Hovering a translated row shows the original body in the tooltip.
  • Right-click again → Show Original reverts.
  • Triggering Translate on three messages in quick succession runs all three in parallel (no serial wait).
  • First-time translation of an uninstalled language pair surfaces the system download prompt.
  • Same-locale messages have no Translate item and no badge.
  • Image / membership / redacted rows have no Translate item.
  • Network off: previously-installed language pair still translates (proves on-device path).
  • Edit lands after translation: stale translation clears.

🤖 Generated with Claude Code

User-initiated translation for received messages using Apple's
Translation framework. The detected source language is checked against
the user's current locale and skipped when already readable; otherwise
the message is queued and translated on-device. The system download
prompt is reached via SwiftUI's `.translationTask` (the only public
entry point that triggers it), with a pool of three slots so up to
three translations run in parallel.

Translated messages get a translate-glyph badge, swap the bubble body
to the translated text, and reveal the original via tooltip. The
context menu and long-press popover both expose Translate / Show
Original. Edit, copy, reply, and quote continue to operate on the
original body — translation is presentation-only and not persisted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant