Skip to content

Add on-device per-message translation#88

Draft
rexbron wants to merge 5 commits into
subpop:mainfrom
rexbron:message-translation
Draft

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

Conversation

@rexbron

@rexbron rexbron commented May 7, 2026

Copy link
Copy Markdown
Contributor

On-device per-message translation

User-initiated, presentation-only translation of received messages using
Apple's Translation framework — entirely on-device, nothing persisted or
sent off the machine.

Behaviour

  • Translate / Show Original in the message context menu. The detected
    source language is checked against your locale and skipped when the message
    is already readable.
  • A translated message swaps its bubble to the translated text, shows a
    translate-glyph badge, and reveals the original on hover.
  • Edit, copy, reply, and quote always operate on the original body.

Implementation

  • RelayKit/Translation/MessageTranslator.swift wraps language detection +
    translation; the view model owns the per-message state
    (idle/loading/translated/failed) and a pending queue.
  • The system model-download prompt is reached via SwiftUI's .translationTask
    (the only public trigger), driven by a pool of 3 parallel slots.
  • Translation state is baked into each MessageRow so the table reloads only
    the affected row when a translation lands.
  • Re-translate: the slot calls Configuration.invalidate() when reusing
    the same language pair, since .translationTask won't re-run for an
    unchanged configuration.

Test plan

  • Translate a foreign-language message → translated text + badge; hover
    shows the original.
  • Show Original, then Translate again → no stuck spinner.
  • A message already in your language offers no Translate action.
  • Copy / reply / edit on a translated message use the original body.

🤖 Generated with Claude Code

rexbron and others added 5 commits May 7, 2026 15:32
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>
# Conflicts:
#	Relay/Views/Message/MessageView.swift
#	Relay/Views/Timeline/MessageGrouping.swift
#	Relay/Views/Timeline/TimelineRowView.swift
#	Relay/Views/Timeline/TimelineTableViewRepresentable.swift
#	Relay/Views/Timeline/TimelineView.swift
# Conflicts:
#	Relay/Views/Message/MessageView.swift
#	Relay/Views/Pickers/ReactionPickerCapsule.swift
#	Relay/Views/Timeline/TimelineRowView.swift
Upstream's reaction-UX redesign removed the inline long-press emoji
popover that hosted MessageView's translate action, leaving
`MessageView.canTranslate` and `onTranslationAction` wired but unused.
Remove both properties and stop passing them from TimelineRowView; the
translate affordance remains in the context menu, and translated text /
the translate badge still render via `MessageView.translation`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`.translationTask(configuration)` only re-runs when the Configuration
value changes, so re-translating a message after reverting to the
original (same source→target pair) built an equal Configuration that
SwiftUI never re-fired — leaving the row stuck on the loading spinner.
The slot now keeps its configuration and calls `invalidate()` (Apple's
re-run mechanism) when the next request reuses the same language pair,
reassigning only when the pair differs.

Also rename the context-menu entry from "Translate" to "Translate
Message".

Co-Authored-By: Claude Opus 4.8 <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