為免反覆複製文字翻譯,作者打造 Mac 工作流程,難在處理跨 app 操作
I got tired of copying text back and forth to translate it. Building the Mac workflow was the harder part.
作者為免在 Mac 上反覆複製文字翻譯,打造原生 Mac app,並發現處理跨 app 工作流程比翻譯本身更費工。雙按 ⌘ 時,有選取文字便顯示翻譯預覽,沒有選取文字則開啟語音輸入;用戶確認後才會替換文字。語音轉錄透過 Soniox 串流處理,翻譯則使用 Apple 系統 Translation framework 或 Gemini,視所選引擎而定。
I often need to translate text while working on my Mac, and one thing that always bothered me was the workflow.
Select some text. Copy it. Open a translator. Paste it. Get the result. Copy it back.
It’s not a big deal once or twice. Doing it repeatedly throughout the day gets annoying.
So I started building a small native Mac app.
The initial idea was simple: select text, press a shortcut, and see a translation without switching apps.
Then I tried to make that work with actual drafts, formatting, changing focus, and voice input.
That’s where most of the work ended up.
A shortcut needs to know what the user is doing
I settled on double-tap ⌘.
With text selected, it opens a translation preview. Without a selection, it opens voice input.
That sounds straightforward, but those two situations need different handling.
A readable selection isn’t necessarily editable. A paragraph on a webpage and a draft in an editor can both be selected, but only one may support replacement.
I had to keep those questions separate:
- Can the app read the selection?
- Can it identify the original editing context?
- Can it safely insert or replace text there?
⠀Treating “I captured some text” as permission to replace it would have been a mistake.
Previewing and replacing should be separate actions
For translation, I wanted the original draft to stay unchanged while the result was being generated.
The flow became:
- Capture the selected text and its context.
- Show the translation.
- Let the user review it.
- Replace the selection only after confirmation.
⠀That extra decision matters when the text includes a deadline, a quantity, or a condition.
For example:
I can send the first draft on Friday. Final delivery depends on the revisions.
A fluent translation can still change the commitment. The interface needs to leave room for checking it.
A preview doesn’t prove the translation is correct. It gives the user a chance to catch a problem before changing the draft.
Text replacement is more than sending a paste command
One of the less obvious problems was deciding whether insertion had actually succeeded.
An editor may normalize paragraphs or represent formatting differently. Comparing every output character and formatting detail too rigidly can report a failure even when the text has arrived.
But assuming success immediately after a paste is also unsafe.
The latest work focused on checking for a change in the original editing context, with the caret in the expected position and the surrounding text unchanged.
When the result is uncertain, the app needs a recovery path. It shouldn’t keep inserting the same text and hope that one attempt works.
A hyperlink is more than its visible label
I also wanted to preserve basic emphasis and actual web links.
Bold and italic text carry useful information. A link has an additional constraint: its visible text can be translated, but its destination should remain the original destination.
I didn’t want a translation model to reconstruct URLs.
The approach I used was to keep the destinations locally and send opaque, per-operation references through the translation request. Returned references are checked against those captured from the source.
An unknown reference is discarded instead of being turned into a guessed link.
This also keeps the distinction between real rich text and Markdown clear. Text that looks like Markdown isn’t automatically treated as a hyperlink.
It’s a small feature from the user’s perspective. Underneath, it needs a fairly strict contract.
Voice input added another review step
Later, I added dictation.
With no text selected, double-tap ⌘ opens voice input. The user can speak, watch the transcript appear, and review the final text.
From there, they can use the original transcript or translate it before insertion.
I keep those stages explicit:
Speech → transcript → optional translation → insertion
Recognizing what someone said and translating it are different operations. If the transcript is already wrong, translating it can make the error harder to notice.
The spoken source language also shouldn’t be inferred from the translation target. Wanting English output doesn’t mean the user is speaking English.
Native UI doesn’t mean everything runs locally
The app uses Swift, AppKit, and SwiftUI. Speech capture uses AVAudioEngine, with streaming transcription through Soniox. Translation uses Apple’s system Translation framework or Gemini, depending on the selected engine.
These paths have different requirements.
Apple translation depends on the macOS version and supported language models. Streaming dictation and Gemini translation require an internet connection.
That distinction needs to be visible to users. A native Mac interface isn’t a reason to describe cloud processing as on-device processing.
What I’d pay attention to earlier next time
I started with the translation request. I’d spend more time on the boundaries earlier now:
- Selection capture and editability are separate.
- Focus can change while an asynchronous request is running.
- Formatting support depends on both the source and destination.
- Insertion needs verification and a recovery path.
- Transcription and translation need separate review.
- Basic formatting preservation isn’t a promise to preserve a whole document layout.
⠀The original idea still feels small to me: make translating a sentence less frustrating.
Most of the engineering has gone into making the steps around that sentence predictable.
If you’ve worked on cross-app text insertion on macOS, I’d be interested in how you handle editors that normalize text after insertion.
Writing disclosure: AI tools assisted with drafting and English editing.
來源:DEV Community · dev.to