Changelog
Every release, in the open.
What shipped, what changed, and any build we had to pull. Pulled straight from GitHub Releases — no curation.
v1.0.0 — First stable release
May 26, 2026View on GitHub →First stable release 🎉
v1.0.0 is a commitment to API + UX stability, not a re-implementation. The bundle is byte-identical in behaviour to v0.11.1 — same code path, same backend wire format, same prompts, same hotkey UX. The 1.0.0 number marks the boundary where the app is feature-complete + production-hardened.
What 1.0.0 covers
- Feature set is complete for every workflow the app was built for:
- Inbound translate (selection → user's primary language) - Outbound translate (per-persona, per-tone) - Contextual rewrite (per-tone bindings + tone picker) - OCR capture (screen region → translate) - App Intents (Shortcuts.app + Spotlight + Siri) - Prompt Engineer mode (VN keywords → EN prompt for AI coding assistants)
- Dual-mode translation backend:
- Bring-your-own-API direct providers: Gemini, OpenAI-compatible, Ollama, DeepL, Google Translate, LibreTranslate - SaaS-hosted Contextual MT Cloud with quota / device cap / per-user encrypted cache
- Distribution pipeline is hardened end-to-end: Apple Developer-ID signed, Apple-notarized, stapled, AppleDouble-clean ZIP + DMG, Sparkle EdDSA auto-update, GitHub Pages workflow deploy for the appcast.
- 400 tests guard the public contract.
What 1.0.0 means going forward
| Bump | When | |---|---| | Patch (1.0.1, 1.0.2, …) | Bug fixes | | Minor (1.1.0, 1.2.0, …) | Backward-compatible feature additions | | Major (2.0.0) | Breaking changes to UserDefaults / Keychain layout / backend wire format |
The on-disk layout (UserDefaults keys, Keychain accounts, file names, signing identifiers) and the backend wire format (
directionfield values, dual-emit body shape, snake_case + camelCase parity) are now considered stable surface area.No behaviour changes vs v0.11.1
Just the version string + this entry. v0.11.0 → v0.11.1 → v1.0.0 upgrade is the cheapest in the app's history.
Install
- Recommended: download the DMG below — open, drag the app into Applications, launch.
- Sparkle auto-updates from v0.10.3+ pull this in within an hour.
Both DMG and ZIP are signed (Developer ID
JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.Thanks
The 6 releases shipped today (v0.10.1 → v1.0.0) closed the long tail of issues that kept the app in alpha: SaaS backend wire format, AppleDouble Gatekeeper trap, TCC permission recovery, transparent-titlebar regression, Input Monitoring removal, SaaS rewrite + prompt expansion, pronoun preservation. The app is ready for daily use.
v0.11.1 — Rewrite pronoun preservation
May 26, 2026View on GitHub →Rewrite pronoun-preservation hotfix
User-reported bug: a draft addressed with "em" was coming back as "anh/chị" (or vice versa) after a rewrite, inverting the entire social relationship between speaker and addressee. In Vietnamese pronouns ARE the relationship — switching them is a correctness failure, not a style choice.
Root cause
The pronoun-preservation rule was a single buried line in a bullet list AND two of the few-shot examples in the prompt itself silently switched "tôi" → "em", actively teaching the model the opposite of what the rule said. The Supabase server prompt didn't mention Vietnamese pronouns at all.
Fixed — Mac app PromptBuilder.rewriteSystemPrompt
- Pronoun-preservation rule promoted from a bullet to an
ABSOLUTE rule — PRONOUNS:top-level section with explicit per-pronoun guarantees:"em" stays "em","tôi" stays "tôi","mình" stays "mình", every addressing pronoun (anh / chị / em / cháu / bạn / mình / quý khách / ông / bà / thầy / cô) preserved verbatim. - Example #2 fixed: input uses "tôi" → output now keeps "tôi" (was wrongly outputting "em").
- Example #3 fixed: input uses "tôi" → output now keeps "tôi" (was wrongly outputting "em").
- New example #4 demonstrates "em → anh" fidelity across a friendly tone shift.
- Other-language formality markers ("tu"/"vous", "tú"/"usted", "你"/"您", "ты"/"вы") added so the fix isn't VN-only.
Fixed — Supabase REWRITE_SYSTEM_PROMPT (already deployed)
- Server-side prompt was about a third the size of the Mac-side prompt and had no pronoun rule at all. Now mirrors PromptBuilder verbatim so SaaS users get the same pronoun-preserving quality as direct-API users.
Tests
rewriteSystemPromptHasFewShotupdated to pin the ABSOLUTE pronoun-rule heading + per-pronoun guarantee strings so a future prompt refactor cannot quietly drop the protection. 400 tests pass.
Risk-free upgrade
UserDefaults + Keychain layout unchanged from v0.11.0.
Install
- Recommended: download the DMG below — open, drag the app into Applications, launch.
- Sparkle auto-updates from v0.10.3+ work.
Both DMG and ZIP are signed (Developer ID
JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.- Pronoun-preservation rule promoted from a bullet to an
v0.11.0 — Prompt Engineer mode
May 26, 2026View on GitHub →Prompt Engineer mode
Type a minimal Vietnamese keyword sketch of a coding task. Press a hotkey. The app pastes back a complete, well-structured English prompt ready for Claude Code, Codex, ChatGPT, or Claude Desktop.
Same hotkey machinery as translate/rewrite — different system prompt, different temperature, different intent. This release makes the use case documented at [contextmt.dev/recipes/prompt-expander](https://contextmt.dev/recipes/prompt-expander) first-class shipping infrastructure.
Added
- `TranslationDirection.expand` enum case + new `PromptBinding` data type (
id,name,hotkey,targetLanguagedefaulten,styleInstructiondefaults to shared template when blank). - Settings → Prompt Engineer section parallel to Contextual rewrite. Per-binding rows: name, target language, optional custom expansion guidelines, hotkey recorder, delete. Gated on
rewriteAvailable. - `TranslationWorkflow.expandAndSend(binding:)` — capture selection → translate with
direction = .expand→ always preview (output is N× longer than input) → paste on confirm. - `AppDelegate` registers
settings.promptBindingson the same gate as rewrite bindings. - `PromptBuilder.expandSystemPrompt` mirrors the Supabase
EXPAND_SYSTEM_PROMPTso direct-API providers (Gemini direct, OpenAI direct) get the same expansion quality without a backend round-trip.
Server side (already deployed)
- Supabase
translateEdge Function acceptsdirection: "expand", swaps toEXPAND_SYSTEM_PROMPT, bumps temperature 0.3 → 0.6, and drops the Source language line from the user prompt. Cache key includes direction so expand / rewrite / translate of the same input get separate slots.
Tests
- +10 new tests —
PromptBindingTests(7: Codable, default fallback, custom precedence,style()pinning, persistence, conflict detection),ExpandDirectionWireFormatTests(2: raw value + JSON shape),BackendStreamingTests(1: end-to-end direction=expand reaches wire body). - 400 tests pass (was 390).
Risk-free upgrade
UserDefaults adds one new key (
translator.promptBindings) defaulting to empty array. Users opt in by adding a binding in Settings → Prompt Engineer. No migration needed.Install
- Recommended: download the DMG below — open, drag the app into Applications, launch.
- Sparkle auto-updates from v0.10.3+ work.
Both DMG and ZIP are signed (Developer ID
JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.How to use
1. Settings → Prompt Engineer → Add binding. A default hotkey (
⌥P) is suggested; change with the recorder if it collides. 2. Optionally open Custom expansion guidelines to override the default template (e.g. pin a specific tech stack: "I work in Swift 6 + SwiftUI on macOS 14+"). 3. From any chat input field (Claude Code, Codex web UI, ChatGPT, etc.), type a minimal sketch in Vietnamese —fix bug login keychain swift, prompt password lặp— then press⌥P. The PreviewHUD shows the expanded prompt; confirm to paste.- `TranslationDirection.expand` enum case + new `PromptBinding` data type (
v0.10.6 — Rewrite on SaaS backend
May 26, 2026View on GitHub →Rewrite works on SaaS backend
User-reported bug: pressing a tone rewrite hotkey (Polite / Professional / Friendly etc.) did nothing when the app was configured to use Contextual MT Cloud or a self-hosted backend. The hotkey registration was silently skipped because the historical
rewriteAvailablegate hardcoded "only direct-API providers can rewrite".Fixed — Mac app
SettingsStore.rewriteAvailablenow returnstruefor bothcustomBackendandfirstPartyBackendtranslation sources. Direct API providers stay gated on each provider's individualsupportsRewriteflag.
Added — SaaS backend (already deployed)
- The Supabase
translateEdge Function accepts a newdirection: "rewrite"field that switches Gemini to a same-language tone-rewrite system prompt and drops Source/Target language lines from the user prompt (otherwise the model would translate VN→VN instead of adjusting tone). - Backward-compatible: any other
directionvalue (including missing or the older"inbound"/"outbound"raw values) lands on the regular translate path. - Cache key includes
directionso a translate-of-X never returns a previously-cached rewrite-of-X.
Internal
- Refactored the v0.10.4 auto-open Settings tests to drive the decision synchronously via a new
checkAutoOpenSettings()method on PermissionManager. Production code still schedules a 1.5 s grace task; tests just exercise the decision body deterministically (the original async version raced the cooperative scheduler in full-suite runs). - 390 tests pass.
Risk-free upgrade
UserDefaults + Keychain layout unchanged from v0.10.5.
Install
- Recommended: download the DMG below — open, drag the app into Applications, launch.
- Sparkle auto-updates from v0.10.3 onwards work.
Both DMG and ZIP are signed (Developer ID
JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.v0.10.5 — Window chrome hotfix
May 26, 2026View on GitHub →Window chrome hotfix
Three app windows (Onboarding, Settings, What's New) carried a transparent-titlebar / clear-background combination from an earlier macOS 26 "Liquid Glass" experiment. On real macOS, the title text rendered straight onto the desktop wallpaper with zero contrast — unreadable. User-visible symptom: top bar fully transparent, app title overlapping window controls and desktop content.
Fixed
- OnboardingWindowController, SettingsWindowController, WhatsNewWindowController — reverted the transparent-titlebar settings. Standard opaque titlebar restored on all three windows. The SwiftUI content's
liquidGlassBackgroundmodifier still applies its material to the content area on macOS 26; only the titlebar chrome changes.
Tests
- Bumped two
PermissionManagertest sleeps 2s → 3s to absorb scheduler jitter that produced intermittent zero-count failures on the auto-open-Settings contract added in v0.10.4. 390 tests pass.
Risk-free upgrade
UserDefaults + Keychain layout unchanged from v0.10.4.
Install
- Recommended: download the DMG below — open, drag the app into Applications, launch.
- Sparkle auto-updates from v0.10.3 onwards work.
Both DMG and ZIP are signed (Developer ID
JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.- OnboardingWindowController, SettingsWindowController, WhatsNewWindowController — reverted the transparent-titlebar settings. Standard opaque titlebar restored on all three windows. The SwiftUI content's
v0.10.4 — Permission UX cleanup
May 26, 2026View on GitHub →Permission UX cleanup
Drops Input Monitoring from the onboarding and Settings surface entirely — the app never actually needed it, and the Request button could not recover after the first denial (macOS silently suppresses the prompt forever once dismissed).
The real fix: this app does not need Input Monitoring at all. Carbon
RegisterEventHotKey(global hotkeys) andCGEventposting (paste-at-caret) both run on Accessibility alone.Removed
- Input Monitoring permission row from onboarding and Settings.
PermissionManager.inputMonitoringGranted+requestInputMonitoringIfNeededdeleted. If a future feature genuinely needs CGEvent tap, the probe + request closure pattern is easy to add back.
Improved
- Auto-open Settings fallback for Accessibility — when
requestAccessibilityIfNeeded()fires and the prompt is silently suppressed (because the user previously denied), a 1.5-second grace task re-checks the live grant. If still not granted, System Settings opens to the Accessibility pane automatically. No more "I clicked Request and nothing happened" dead end.
Build pipeline
scripts/package_dmg.shnow codesigns the DMG wrapper with Developer ID before notarization. v0.10.3's manually-built DMG had this; the script did not.
Tests
PermissionManagerTestsdrops 2 Input Monitoring assertions and adds 2 new ones pinning the auto-open Settings contract: fires when grant misses the grace window; skipped when grant arrives in time. 390 tests pass (was 388, +2).
Risk-free upgrade
UserDefaults + Keychain layout unchanged from v0.10.3.
Install
- Recommended: download the DMG below — open, drag the app into Applications, launch.
- Sparkle auto-updates from v0.10.3 onwards work too (pulls the ZIP).
Both the DMG and the ZIP are signed (Developer ID
JP4XV8A9JF), notarized by Apple, and stapled. Apple Silicon only. macOS 14 or later.v0.10.3 — Distribution hotfix (Gatekeeper acceptance)
May 26, 2026View on GitHub →Distribution hotfix — Gatekeeper acceptance
v0.10.0, v0.10.1, and v0.10.2 all shipped distribution ZIPs that, on the user's machine, failed Gatekeeper verification with "_sealed resource is missing or invalid_" — surfacing as "app chưa sign" when Sparkle tried to install an update.
The bug: hidden AppleDouble metadata files (the
._*files) inside the embeddedSparkle.framework, introduced by SwiftPM's xcframework extraction on macOS. They were invisible tocodesign --sign(which signs the real files), so the build chain succeeded — but post-extractioncodesign --verify --deep --stricttreated them as foreign content and rejected the bundle.v0.10.3 carries zero application code changes — it is purely the v0.10.2 codebase re-packaged with the build pipeline fixed.
How to install
Recommended: download the v0.10.3 ZIP from this page — Sparkle on a v0.10.0 install that already aborted a v0.10.1/v0.10.2 update is likely in an inconsistent state. Unzip, drag the
.appinto/Applicationsreplacing the old copy, run it. Future Sparkle updates (v0.10.4 onwards) install cleanly through Sparkle.If Sparkle's Check for Updates does work — even better. The v0.10.3 bundle is correctly signed + notarized + stapled, and the distribution ZIP no longer contains the offending metadata.
Build pipeline fixes
scripts/package_app.shstrips._*files BEFORE and AFTER the codesign chain (defense in depth).- The script's final step runs
codesign --verify --deep --strictas an invariant check — if a future toolchain combination re-introduces the problem, the build fails loudly. - The distribution ZIP is now created with
ditto --norsrc --noextattr --noaclso that ditto does not re-pack metadata into the ZIP via xattr packaging.
Verified end-to-end
``
$ unzip Contextual-Mac-Translator-v0.10.3-macos-arm64.zip $ find "Contextual Mac Translator.app" -name "._*" | wc -l 0 $ codesign --verify --deep --strict --verbose=2 "Contextual Mac Translator.app" valid on disk; satisfies its Designated Requirement $ spctl --assess --type execute --verbose "Contextual Mac Translator.app" accepted; source=Notarized Developer ID $ xcrun stapler validate "Contextual Mac Translator.app" The validate action worked!``Apple Silicon only. macOS 14 or later.
v0.10.2 — Permission UX & TCC stability
May 26, 2026View on GitHub →Permission UX & TCC stability
A user-reported pain point: every Sparkle upgrade felt like macOS was clearing the app's Accessibility grant — translate hotkeys went silent until you manually re-granted, with no in-app surface telling you what happened.
v0.10.2 fixes the user experience and hardens the signature so the underlying TCC issue is much less likely to recur in the first place.
Added — Auto-recovery onboarding
- Permission-loss detection on launch —
AppDelegatereads the persistedlastKnownAccessibilityGrantedflag _before_ refreshing the live grant, so the true→false transition that signals a TCC reset is detectable. When detected, the onboarding window pops automatically in recovery mode with copy that acknowledges you already did this once. - `OnboardingMode.permissionRecovery` swaps the title + intro: _"Welcome back. macOS cleared the app's Accessibility grant after the recent update — translate hotkeys are silent until you re-grant. Click Request below, or Open Settings if the prompt does not appear."_
- Hotkeys still register in recovery mode so any grants that survived (e.g. Input Monitoring) keep working — the onboarding pops on top so the issue cannot be missed.
Hardened — Codesign stability
--identifier "app.lookerlab.translator"is now passed explicitly to the main-appcodesigncall instead of being inferred fromCFBundleIdentifier. The value baked into the signature is byte-stable across releases. Verified post-build: v0.10.0, v0.10.1, and v0.10.2 all sign with byte-identical designated requirements, so TCC should persist the grant across upgrade.
Internal —
PermissionManagertestability- Constructor accepts injected
accessibilityProbe/inputMonitoringProbe/requestAccessibilityAction/requestInputMonitoringActionclosures so unit tests exercise grant-sync wiring without touching the real TCC database. refresh()writes the live grant back toSettingsStore.lastKnownAccessibilityGranted(guarded bydidSetso the 1-second polling loop does not spam UserDefaults).refreshLater()settle delay bumped 1s → 2s — gives the TCC database room after the user clicks "Allow".
Tests
- +11 new tests — 6 in
PermissionManagerTests(grant-sync, init non-mutation, request action, nil-settings), 5 inLaunchRecoveryDecisionTreeTests(fresh / steady / revoked / never-granted / lost-then-regranted). - 388 tests pass (was 377).
Risk-free upgrade
UserDefaults adds one new key (
translator.lastKnownAccessibilityGranted) defaulting tofalseon existing installs — so the recovery flow does not fire spuriously on the v0.10.1 → v0.10.2 upgrade itself. The first refresh writes the current live state forward; from v0.10.3 onwards the recovery signal is armed end-to-end.Install
- Sparkle — existing installs receive this automatically via the appcast at
https://hoangperry.github.io/hp-translator/appcast.xml. - Fresh install — download
Contextual-Mac-Translator-v0.10.2-macos-arm64.zipbelow, unzip, drag the.appto/Applications. Signed (Developer IDJP4XV8A9JF) + notarized + stapled.
Apple Silicon only. macOS 14 or later.
- Permission-loss detection on launch —
v0.10.1 — SaaS backend hotfix
May 26, 2026View on GitHub →SaaS backend hotfix
v0.10.0 shipped right as the production SaaS Edge Function at
app.contextmt.devwent live, and the two wire formats turned out to be incompatible. Translates failed immediately withHTTP 400: target_language is required; once that cleared, a second failure surfaced — the streaming flow expected SSE on/translate/stream, but Supabase returns one-shot JSON, so the stream loop never saw adata:-prefixed frame and threw _"The backend response did not include a translation."_Both fixes are backward-compatible with the legacy self-hosted FastAPI server.
Fixed
- Dual wire format —
BackendRequestBodynow emits every routing field under both camelCase (legacy self-hosted FastAPI intranslator-server/server.py) and snake_case (SaaS Supabase Edge Function). One payload, both backends. Pinned by a newbodyEmitsDualNamingConventionscontract test so a future refactor cannot silently re-break one backend. - Streaming non-SSE fallback —
BackendProvider.streamSSEsniffsContent-Type; if it's nottext/event-stream(Supabase returnsapplication/json), the body is drained and decoded as a one-shotTranslationResultthen emitted as a single.done(...)update. Self-hosted SSE keeps its real streaming UX. Pinned bynonSSEJSONFallbacktest.
Tests
- 377 tests pass (was 376; +1 fallback test).
- Removed a stale
"0.10.0"entry fromWhatsNewHighlightsTests.unknownVersionReturnsNil— v0.10.0 added its own highlight set so the value moved out of the catch-all bucket.
Risk-free upgrade
UserDefaults + Keychain layout byte-identical to v0.10.0. Existing settings, hotkey bindings, register card, and glossary entries all round-trip clean.
Install
- Sparkle — existing v0.10.0 installs will receive this update automatically via the appcast at
https://hoangperry.github.io/hp-translator/appcast.xml. - Fresh install — download
Contextual-Mac-Translator-v0.10.1-macos-arm64.zipbelow, unzip, drag the.appto/Applications. Signed (Developer IDJP4XV8A9JF) + notarized + stapled.
Apple Silicon only. macOS 14 or later.
- Dual wire format —
v0.10.0 — Cultural Precision & Privacy (VN Register + Privacy badge + Glossary v2)
May 25, 2026View on GitHub →Cultural Precision & Privacy
The first feature-loaded minor since v0.9.0. Three new sub-features anchored on "Apple Intelligence can't do per-locale Vietnamese precision" — pin your social register, see the privacy class of every translation, and replace the free-text glossary blob with typed entries the LLM follows exactly. Risk-free upgrade — 375/79 tests stay GREEN, every v0.9.x setting + binding + hotkey + glossary blob round-trips byte-identical, Sparkle OTA from v0.6.1+ users auto-updates with no manual action.
Added — VN Social Register Card (anchor)
- `RegisterCard` with 3 axes — Dialect (Bắc/Nam), Kinship (anh/chị/em/cháu/bạn), Formality (formal/neutral/casual) — plus optional roleHint ("e.g. TikTok Shop seller addressing customer").
- Settings → Contextual Rewrite → Vietnamese register card — 3 Picker dropdowns, roleHint TextField, Reset button, live composed-prompt preview so you see exactly what the LLM receives.
- Composition is prepend, not replace — your per-binding tone instructions flow through unchanged below the
[Tone]tag. nil= disabled (default) → v0.9.x behaviour byte-identical for users who never open the panel.
Added — Local-LLM Privacy mode
- `ProviderPrivacyClass` enum (
.local/.cloud/.hosted) — every provider classifies explicitly. Ollama =.local, BackendProvider =.hosted, everything else (Gemini direct + CLI, Codex CLI, DeepL, Google Translate, LibreTranslate, OpenAI-compatible) =.cloud. - PreviewHUD Privacy badge — every translation surfaces the provider's class (🛡 Local / ☁ Cloud / 🏢 Hosted) with
.help()tooltip showing the full provider name. Stamped eagerly intoTranslationStyleat workflow construction so SwiftUI render never reaches back intoproviderFactory(). - Settings → Privacy section — active-provider ribbon with Vietnamese headline ("🛡 Local only — không gửi dữ liệu khách ra nước ngoài" when Ollama is active), collapsed Ollama onboarding card with download link + two curated
ollama pullcommands (each with a Copy button) + Browse-more-models link + one-click "Test Ollama connection" button (2s timeout, exact-name model match via/api/tagsJSON parse, inline green/red status).
Added — Glossary v2-Lite (typed entries)
- `GlossaryEntry` Codable with 3 kinds —
.dontTranslate(term:)for brand names / code identifiers,.alias(from:to:)for casing & spelling normalisations,.alwaysTranslate(term:to:)for jargon. - Forward-compatible partial recovery — unknown KindTag from a future v0.10.x is dropped at the element level (not the whole list) via
GlossaryEntry.decodeArray(from:). A user who briefly opens v0.10.1, adds one.scopedentry, then downgrades, keeps every other v0.10.0 entry intact. - `GlossaryComposer` pure function composes typed entries + legacy free-text blob.
dontTranslategrouped under one bullet;alias+alwaysTranslaterender one-per-line directional pairs. 50-entry render cap. - Settings → Glossary editor — Add-menu for the 3 kinds, type pill tinted per kind, 1-or-2 TextFields per row, delete button, drag-to-reorder via
.onMove. Legacy free-text TextEditor moved to a collapsed DisclosureGroup below; rules apply structured-first.
What's-New on first launch
A small window pops once for v0.10.0 upgrades with 3 highlight cards (Register Card / Privacy badge / Glossary v2). Doesn't steal focus — click "Got it" to dismiss.
Compatibility
- Every v0.9.x persisted state round-trips clean:
RewriteBinding,HotkeyConfig,TranslationStyle,SaaSConfig, legacy glossary string blob — all unchanged in storage. TranslationStylegains 3 default-safe fields (registerCard: nil,privacyClass: nil,providerDisplayName: "").- macOS 14 minimum preserved.
Install
- Sparkle OTA: v0.6.1+ users auto-update from inside the app — no action needed.
- Fresh install: download the DMG below → drag to /Applications.
Verify
- App: 375 Swift / 79 suites GREEN (+56 from v0.9.2's 319).
- Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.
Deliver-phase code review
Independent code-review agent before tag: 0 CRITICAL / 3 HIGH / 4 MED / 2 LOW. All 3 HIGH + 3 MED fixed inline (commit
285f519):- H1 —
SettingsPrivacySectionwas allocatingTranslationProviderFactoryon every SwiftUI render. Cached in@State, refreshed only on provider-change. - H2 — Force-unwrapped URL literals in body position. Lifted to
static letconstants. - H3 — Glossary array decode was all-or-nothing; one unknown KindTag from a future v0.10.x would nuke the user's whole list. Switched to per-element partial-recovery decode.
- M1 — Concurrent taps of "Test Ollama connection" could race on
testResult. AddedisTestingguard. - M2 —
RegisterCard.promptedwas appending raw prefix instead of trimmed prefix, opening a double-blank gap when callers passed trailing whitespace. - M3 — Ollama
/api/tagsmodel match was substring (bodyText.contains(...)) which gave false positives. Switched to JSON-parsed exact-name match.
Deferred to v0.10.1: M4 (
stamp()helper duplication betweenTranslationWorkflow+CaptureOrchestrator), L1 (roleHint character-vs-byte cap clarification), L2 (LibreTranslate self-hosted shows ☁ Cloud despite localhost endpoint).Roadmap (next)
- v0.10.1 patch backlog: M4 + L1 + L2 above, per-binding register-card override toggle (carried from v0.10.0 deferred list).
- v0.11.0 candidates: voice input via WhisperKit (Moonshine-Medium is production-ready for VN by 2026), Glossary v2 scoped entries (per-language + per-app).
EOF
v0.9.2 — Second refactor patch (SaaSConfig extract + protocol alignment)
May 24, 2026View on GitHub →Second refactor patch
No new user-facing features. Clears the remaining HIGH + MED + LOW backlog items from the v0.9.0 deliver-phase review that v0.9.1 deferred. Risk-free upgrade — 319/69 tests stay GREEN with the same test bodies; UserDefaults + Keychain layout is byte-identical to v0.9.1 so every setting, hotkey, binding, and SaaS session round-trips clean.
Changed (internal)
- `SettingsStore` god-object trimmed (W1, HIGH) — extracted the Supabase project URL + anon key + the 4 SaaS factory helpers (
authConfig/makeSessionStore/translateEndpoint/deviceIdentity) into a newAuth/SaaSConfig.swiftfile.SettingsStoredrops from 593 → 538 LOC and from owning 6 Supabase concepts down to 1 (just asaaSConfigreference). Future SaaS-only changes now live in one file. - `ScreenCaptureService` protocol shape aligned to `OCREngine` (MED-4) — dropped the unnecessary
@MainActor+AnyObjectrequirements from the protocol. Production impl is still@MainActorwhere it needs to be; stubs in tests can now be plain structs. Cleans up the asymmetry the v0.9.0 review flagged.
Fixed
- Picker + capture hotkey recorder sheets share one helper now (LOW-2) — centralised the synthetic
Binding<HotkeyConfig>pattern intooptionalHotkeyBinding(_:fallback:)onSettingsView. Pre-allocatedHotkeyConfig.defaultPicker/.defaultCaptureconstants replace the per-render allocations.
Deferred to v0.9.3+
TranslationWorkflowClipboardRestoreTest— needsHUDPresenting+Pasteboardingprotocol extractions (own refactor commit).- Hardcoded Supabase anon-key + URL move to build-time
.xcconfig(now thatSaaSConfigis its own file, this becomes a one-line init-default change + a CI secret wiring step).
Install
- Sparkle OTA: v0.6.1+ users auto-update from inside the app — no action needed.
- Fresh install: download the DMG below → drag to /Applications.
Verify
- App: 319 Swift / 69 suites GREEN (unchanged from v0.9.1).
- Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.
- `SettingsStore` god-object trimmed (W1, HIGH) — extracted the Supabase project URL + anon key + the 4 SaaS factory helpers (
v0.9.1 — Refactor + reliability patch
May 24, 2026View on GitHub →Refactor + reliability patch
No new features. Internal cleanup for the two pieces of structural debt the v0.9.0 deliver-phase review flagged, plus 13 high-leverage tests covering paths that silently failed before. Upgrade is risk-free — every v0.9.0 setting, hotkey, binding, and toggle round-trips clean; all 306 existing tests stay GREEN with zero modifications.
Changed (internal)
- `TranslationWorkflow.swift` split: 876 → 630 LOC, back under the 800-line guideline before any new workflow path lands.
- New `CaptureOrchestrator.swift` — owns the v0.9.0 OCR-translate flow end-to-end. - New `RewriteService.swift` — owns the rewrite primitives (single-shot + refusal-retry + multi-variant + the 3 App Intents headless methods + the
PickerEntry → TranslationStylemapper). Previously duplicated in multiple sites; now one truth.- Behaviour byte-identical. Public API of
TranslationWorkflowunchanged.
Fixed
- No more silent Japanese fallback in the status-bar "Send Keigo" / "Send Casual" menu items. A Vietnamese-primary user with no formal/casual outbound binding used to get a hardcoded
.japaneseBusiness/.japaneseCasualtranslation. v0.9.1 surfaces an actionable error toast pointing to Settings → Translation bindings.
Added (tests)
- `BackendStreamingURLTests` (9 tests) — pin every branch of
BackendProvider.streamingURL(for:). The 4-case path rewriter used to silently fall back to the configured endpoint on edge cases (/api/translate, trailing slash, non-default port); tests now lock the behaviour. Required markingstreamingURLnonisolated static(honest — pure URL logic, no actor state). - `WhatsNewHighlightsTests` (4 tests) — pin the per-version highlight lookup that v0.9.0 introduced. Verifies v0.10.0 / v0.9.2 / older patches all fall into the
nilcatch-all branch correctly, so a future build can't silently mis-show stale highlights.
Deferred to v0.9.2
TranslationWorkflowClipboardRestoreTest— needsHUDPresenting+Pasteboardingprotocol extraction (own refactor commit).SettingsStoregod-object split (extractSaaSConfig).OCREngineprotocol isolation alignment.
Install
- Sparkle OTA: v0.6.1+ users auto-update from inside the app — no action needed.
- Fresh install: download the DMG below → drag to /Applications.
Verify
- App: 319 Swift / 69 suites GREEN (+13 from v0.9.0's 306).
- Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.
v0.9.0 — Input Surface Expansion (Shortcuts.app + OCR-from-screen)
May 24, 2026View on GitHub →Input Surface Expansion
Two new ways to invoke the translator: from any app via Shortcuts.app / Spotlight / Siri, and from any pixel on screen via an OCR capture hotkey. Both are opt-in. The single-key inbound/outbound flow + the v0.8 tone picker + the v0.8.5 multi-variant rewrite all stay byte-identical.
What's new
#### Shortcuts.app, Spotlight & Siri (App Intents)
Three new actions discoverable in Shortcuts.app under "Contextual Mac Translator":
- Translate Text — translate any text. Target language optional (falls back to your primary language). Returns a string usable in subsequent Shortcut steps.
- Rewrite with Tone — picks from the 7 preset tones (Polite, Professional, Friendly, Firm-but-polite, De-escalate, Concise, Custom). Use it to chain the translator into Quick Actions, Raycast, Alfred, calendar workflows, anything that speaks AppIntents.
- Rewrite with Instruction — free-text instruction ("warmer reply", "shorter under 2 sentences"). Mirrors the in-app picker's freetext row from v0.8.3.
Spotlight + Siri trigger phrases prefixed with "Contextual" so they don't collide with Apple Translate.
#### OCR-from-screen translate
- Bind a hotkey in Settings → Capture, drag the system crosshair (same UX as Cmd+Shift+4), and the app reads the text on-device via Vision, auto-detects source language via NaturalLanguage, and translates it into your primary language.
- Recognises Vietnamese / English / Simplified Chinese / Japanese / Korean — picked for the VN-power-user audience (1688 / Aliwangwang / Messenger / Discord flows where browser translation breaks down).
- Result opens in a copy-mode PreviewHUD: the Send button becomes "Copy". No auto-paste, no keystroke simulation. You read the translation; you click Copy if you want it on your clipboard.
- Privacy: OCR runs entirely on-device. Only the recognised text travels to your active translation provider — same as every other translate flow. Screen pixels never leave the device.
#### What's-New window
Pops once on first launch of a new minor/major release with a short summary of what's worth knowing. Self-dismissible. Doesn't steal focus (you can keep typing while it shows).
How to set up
1. Shortcuts: open Shortcuts.app → New Shortcut → search for "Translate Text" / "Rewrite with Tone" / "Rewrite with Instruction" under Contextual Mac Translator. 2. OCR: Settings → Capture → tick "OCR capture hotkey" → record a hotkey (default suggestion ⌘⌥G). The first invocation triggers macOS's Screen Recording permission prompt; the Settings panel has a one-click link to System Settings if you missed it.
Compatibility
- All v0.8.5 persisted state round-trips clean (RewriteBindings / hotkeys / glossary / expressive-tones / multi-variant — every Codable contract unchanged).
- Sparkle OTA from v0.6.1+ auto-updates to v0.9.0 with no user action.
- macOS 14 minimum preserved. App Intents (macOS 13+), Vision text recognition (10.15+), ScreenCaptureKit (12.3+), NaturalLanguage (10.14+) all backward-compatible to the floor.
Install
- Sparkle OTA: v0.6.1+ users auto-update from inside the app.
- Fresh install: download the DMG below → drag to /Applications.
Verify
- App: 306 Swift / 67 suites GREEN (+36 from 270 in v0.8.5).
- Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.
- Code-review pass: 0 CRITICAL / 3 HIGH (all fixed before tag) / 4 MEDIUM (2 fixed, 2 carried forward as test-coverage backlog).
Roadmap (next)
- v0.9.1 patch backlog: per-binding multi-variant override,
captureAndTranslateorchestrator test, screencapture timeout-path test, MED-4 OCREngine isolation alignment. - v0.10.0 candidates from discovery: Glossary v2 typed entries, VN Register Card UI, Local-LLM "Privacy mode" pivot.
Acknowledgements
Discovery + Define + Develop + Deliver phases run via the OCTO Double-Diamond workflow. Planning artefacts at
docs/v0.9.0/{discovery,define}.md.v0.8.5 — Multi-variant rewrite (3 drafts in one round-trip)
May 23, 2026View on GitHub →Multi-variant rewrite — 3 drafts in one round-trip
Opt-in setting that asks the LLM for three different rewrites in a single network call, then lets you page through them in the preview HUD before sending.
What's new
- Generate 3 drafts per rewrite toggle under Settings → Contextual rewrite. Off by default — existing users stay on the cheaper single-draft path.
- Multi-variant preview HUD — pager chip ("2 / 3"), ← / → buttons to flip between drafts, ⌘1–3 for quick-select, edit any draft with Tab (edits persist per-variant as you page back and forth).
- One network call — the prompt asks for N drafts separated by a
---VARIANT---sentinel, so cost is ~1.5–2× tokens but only one round-trip latency. - Graceful fallback — if the model ignores the multi-variant prompt or all drafts get filtered as refusals, the existing single-draft retry chain kicks in so you still get a result.
How to use
1. Settings → Contextual rewrite → tick "Generate 3 drafts per rewrite". 2. Invoke a rewrite the same way you always have — binding hotkey (⌥R / ⌥E / …) or the tone picker (⌘⌥⏎). 3. The preview HUD shows draft 1/3. Use ← / →, ⌘1–3, or the chevron buttons to switch. 4. Tab to edit the current draft. Return to send the active one. Esc to cancel.
Compatibility
- Default OFF — no existing users see a behaviour change without opting in.
- Single-draft mode (toggle OFF) takes the same code path as v0.8.4 and earlier.
- Works with every LLM provider (Gemini, Ollama, OpenAI-compatible).
Install
- Sparkle OTA: v0.6.1+ users auto-update from inside the app.
- Fresh install: download the DMG below → drag to /Applications.
Verify
- App: 270 Swift / 56 suites GREEN (+13 net: multi-variant view-model, variant splitter, style helpers).
- Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.
Roadmap (next)
- Polish on response parsing for the long tail of models that ignore the sentinel in creative ways.
- Optional per-binding multi-variant override (today the toggle is global).
v0.8.4 — Pre-warmed picker + per-binding 'In picker' + a11y
May 23, 2026View on GitHub →Pre-warmed picker + per-binding "In picker" + VoiceOver polish
Three small upgrades that make the tone picker feel native.
What's new
- Pre-warmed panel — the picker NSPanel is constructed at app launch instead of on first hotkey press, so first open is near-instant. The ~30–60 ms setup cost moves off the hotkey path.
- "In picker" per binding — every saved rewrite binding in Settings now has an opt-in checkbox to also surface it as a row in the popup. Pick saved instructions (with your custom override prompts) without remembering hotkeys.
- VoiceOver labels — every picker row announces its name + ⌘ shortcut hint + selected state; the container itself announces "Tone picker — Type to filter, arrow keys to navigate, Return to apply, Escape to cancel".
How to use
- In picker checkbox: Settings → Contextual rewrite → expand any binding row → tick "In picker" (default ON). Open the picker with your tone-picker hotkey; saved bindings appear below built-in tones.
- Search: type to filter; bindings match by their display name + tone label.
- Quick-select: ⌘1–9 commits the Nth row directly, including bindings.
Compatibility
- Bindings persisted by v0.8.3 and earlier decode with
showInPicker = trueviadecodeIfPresent— zero migration cost, round-trip safe. - Users who want a binding hidden from the picker can untick it in Settings.
Install
- Sparkle OTA: v0.6.1+ users auto-update from inside the app.
- Fresh install: download the DMG below → drag to /Applications.
Verify
- App: 257 Swift / 54 suites GREEN (pre-existing flaky
SupabaseAuth.sendCode-no-configtest ignored — passes 10/10 in isolation, fails ~1× per full suite since v0.5.x). - Build: signed (Developer ID), notarized + stapled (DMG + Sparkle zip), EdDSA-signed appcast.
Roadmap
- v0.8.5 — Multi-variant rewrite (generate 3 drafts in one call, pick between them via Preview HUD).
v0.8.3 — Free-text custom instruction in tone picker
May 23, 2026View on GitHub →Free-text rewrite instructions in the tone picker — Apple Writing Tools "Describe your change" style, kept inside the same one-hotkey flow.
How to use
Press your picker hotkey → start typing anything that's not a preset name → a "Use: ..." row appears at the top → Return / ⌘1 / click to commit. The app rewrites with your typed instruction.
Examples:
- "make it sound less defensive"
- "warmer reply to an angry client"
- "shorter, under 2 sentences"
- "as if I'm writing to my boss"
Added
- `PickerEntry` sum type —
.preset(RewriteTone)for built-in tones,.freetext(String)for user-supplied instructions. - Freetext row appears at the top of the picker whenever the query is non-empty, with a pencil icon and italic label.
- Field placeholder updated: "Filter or describe…" makes the dual purpose discoverable.
Notes
- Freetext rewrites stay strict — they don't auto-flip the expressive-content flag. Users who want
Chửi thềBLOCK_NONE routing should pick that preset explicitly. - Tests: 252 Swift / 53 suites GREEN (+2 net from picker test rework)
- Apple notarization: DMG + zip both Accepted and stapled
Auto-update
On v0.6.1+ this arrives automatically via Sparkle.
v0.8.2 — New tone: Chửi thề (casual-raw, opt-in)
May 23, 2026View on GitHub →New rewrite tone: "Chửi thề" — opt-in casual-with-edge register for Vietnamese chats with close friends. Uses profanity markers like vl / vcl / đm as natural intensifiers (the "as hell" / "af" of Vietnamese internet writing), preferring abbreviated forms over spelled-out vulgar phrases.
For chats with friends, not customers. Every rewrite still shows in preview before sending.
How to enable
1. Settings → Contextual rewrite → "Enable expressive tones (Chửi thề)" — flip on 2. A confirmation dialog appears explaining the tone's intent — hit Continue 3. "Chửi thề" now appears in the tone picker + binding dropdown
Safety + design notes
- Identity-safe. The prompt has hard carve-outs: never add slurs, never attack identity (race, gender, origin, appearance), never add personal insults that aren't already in your input.
- Abbreviation-first. The prompt nudges toward
vl / vcl / đmover spelled-out vulgar phrases — matches authentic Vietnamese internet chat register. - Provider routing. With Gemini, the app sets
safetySettings = BLOCK_NONEfor the 4 adjustable categories (HARASSMENT / HATE_SPEECH / SEXUALLY_EXPLICIT / DANGEROUS_CONTENT) — Google explicitly documents this for creative-writing use cases. Non-adjustable filters (CSAM, election integrity) remain enforced. - Other providers may refuse. OpenAI / Ollama / etc. don't get the BLOCK_NONE relaxation (their content policy varies). The existing refusal-detection retry kicks in; if the model still refuses, the workflow falls back cleanly without overwriting your draft.
- Opt-in by default. The toggle is OFF on fresh installs. Updating from v0.8.1 keeps it OFF — nothing surprising.
Auto-update
On v0.6.1+ this arrives automatically via Sparkle.
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 250 Swift tests / 53 suites (+8 new: casualRaw flag, available() filter, expressive style flag, Gemini permissive safety shape, Settings persistence, Vietnamese instruction guidance)
- Apple notarization: DMG + zip both Accepted and stapled
v0.8.1 — tone picker code-review polish
May 23, 2026View on GitHub →Code-review polish on top of v0.8.0 — no behaviour change, pure cleanup.
Changed
- Single source of truth for the picker panel size.
TonePickerControllerderives both theNSPanel.contentRectat construction AND the cursor-anchored origin from oneprivate let panelSizeconstant; the two can no longer drift. - Removed unused `clampSelectionAfterFilter()` helper (and its test). The view already resets the selection to 0 on every filter change (the Spotlight/Raycast UX) — the clamp helper would have been a different UX and was dead code.
Auto-update
On v0.6.1+ this arrives automatically via Sparkle.
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 242 Swift tests / 52 suites GREEN (−1 from the removed clamp test)
- Apple notarization: DMG + zip both Accepted and stapled
- Single source of truth for the picker panel size.
v0.8.0 — Tone picker hotkey: one popup, every tone
May 23, 2026View on GitHub →New feature: Tone picker hotkey. Bind ONE global hotkey and a popup picker lists every rewrite tone. Pick with arrows + Return, type-to-filter, or ⌘ 1-7. The app rewrites your current input line in that tone, shows the preview HUD, and pastes when you confirm.
Coexists with v0.7's per-binding model — power users keep their muscle-memory hotkeys for top tones, the picker scales to the long tail.
How to use
1. Settings → Contextual rewrite → "Tone picker hotkey" → Set hotkey (e.g. ⌘⌥⏎) 2. Type a message in any app 3. Press the picker hotkey — popup appears at your cursor 4. Pick a tone (arrows + Return /
⌘ 1-7/ type to filter) 5. Review the rewrite in the preview HUD, edit if needed, hit Send| Picker key | Action | |------------|--------| | ↑ / ↓ | Move selection | |
Return| Confirm tone | |Esc| Cancel | |⌘ 1-7| Quick-select that row | | Click outside / 5 s focus loss / 20 s dwell | Auto-dismiss | | Re-press picker hotkey | Toggle picker closed |Safety + privacy
- Refuses password fields. AX role check blocks
AXSecureTextFieldbefore any keyboard simulation runs — your password drafts never reach an LLM. - Eager clipboard restore. The captured line is restored to the clipboard *before* the picker dwells, so a clipboard manager doesn't snapshot your draft.
- Right-Arrow collapse. Immediately after Cmd+C the selection is collapsed — a cancelled picker leaves zero side effects in your input field.
- Always preview. Tone-changed messages always go through the preview HUD before paste — never auto-send.
Provider requirements
Needs an LLM provider: Gemini, Ollama, or an OpenAI-compatible API. DeepL and Google Translate cannot rewrite — Settings warns when the active provider can't, and the picker hotkey simply doesn't register.
Auto-update
On v0.6.1+ this arrives automatically via Sparkle — the app prompts with "Install and Relaunch."
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 243 Swift tests / 52 suites GREEN
- Apple notarization: DMG + zip both Accepted and stapled
- Refuses password fields. AX role check blocks
v0.7.1 — Contextual rewrite polish (clipboard race + hotkey gate)
May 23, 2026View on GitHub →Polish on top of v0.7.0. Addresses the post-ship code-review findings — no new features.
Fixed
- Clipboard race on post-paste focus loss. Both
translateAndSendandrewriteAndSendnow use the delayedrestoreClipboard(snapshot)(700 ms) when the focus-guard fails *after* the paste, instead of the synchronous restore. A slow target app could otherwise read the restored snapshot before it had finished consuming the just-pasted text. - Rewrite hotkey gated by provider availability.
applyHotKeysnow skips registering rewrite hotkeys when the active provider can't rewrite (DeepL / Google Translate / LibreTranslate). Switching back to an LLM provider re-registers automatically. No more "press hotkey, get an error every time" while a non-LLM provider is selected.
Auto-update
If you are on v0.6.1+, this arrives automatically via Sparkle.
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 220 Swift tests / 49 suites GREEN
- Apple notarization: DMG + zip both Accepted and stapled
- Clipboard race on post-paste focus loss. Both
v0.7.0 — Contextual rewrite: same-language tone shift on a hotkey
May 23, 2026View on GitHub →New feature: Contextual rewrite. Bind a hotkey to a tone and the app rewrites the current input line in that tone — same language, intent preserved. Built for the moment you typed something blunt or angry and need to soften it before sending. Always shows a preview before sending — never auto-paste.
How to use
1. Settings → Contextual rewrite → Add binding 2. Pick a tone — Polite, Professional, De-escalate, Friendly, Firm-but-polite, Concise, or Custom (your own free-text instruction) 3. Click Change… to record a hotkey (e.g. ⌥R) 4. Type a message that needs softening, press the hotkey 5. Review the rewritten version in the preview HUD, edit if needed, hit Send
Example:
| You typed | Tone | App rewrites | |-----------|------|--------------| | Chị ngu quá chị không hiểu gì hết | Polite | Chị ơi, chỗ này hơi khó hiểu một chút, để em giải thích lại rõ hơn cho mình nhé. | | Trả tiền đi không tôi không giao hàng nữa đâu | Firm but polite | Anh/chị vui lòng hoàn tất thanh toán giúp em ạ, để bên em sắp xếp giao hàng đúng hẹn nhé. | | Lỗi của shipper chứ đâu phải lỗi của tôi mà bắt đền | De-escalate | Em rất xin lỗi về sự cố vừa rồi ạ. Trường hợp này phát sinh từ khâu vận chuyển, em sẽ hỗ trợ kiểm tra và xử lý sớm cho mình. |
Vietnamese-aware
The prompt preserves your existing anh/chị/em pronoun pairing exactly and adds politeness particles (ạ / dạ / nhé) as the tone requires.
Provider requirements
Rewrite needs an LLM provider — Gemini, Ollama, or any OpenAI-compatible API. DeepL and Google Translate cannot rewrite (translation-only, no instruction following) — Settings shows a warning when the active provider can't.
Safety
- Always preview. A tone-changed message must be reviewed before it goes out.
- Refusal handling. If the model declines on a profane draft, the app retries once with a stronger anti-refusal prompt; if it still refuses, your original text is kept and you get a clear error.
- Original-text guard. Empty custom binding? Wrong provider? Network error? The app fails fast with an actionable message and never overwrites your input with garbage.
Auto-update
If you are on v0.6.1+, this arrives automatically via Sparkle — the app offers v0.7.0 with an "Install and Relaunch" prompt.
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 220 Swift tests / 50 suites GREEN
- Apple notarization: DMG + zip both Accepted and stapled
v0.6.3 — real Liquid Glass for app windows
May 22, 2026View on GitHub →Corrects the Liquid Glass adoption. v0.6.1/v0.6.2 made the onboarding and Settings windows translucent with
.ultraThinMaterial— but that is the *legacy* vibrancy material (the pre-Tahoe fallback), not Liquid Glass. On macOS 26 those windows were rendering the old material.Fixed
- Onboarding + Settings windows now use real Liquid Glass — swapped the unconditional
.ultraThinMaterialfor a helper that resolves to.glassEffect()on macOS 26 Tahoe and falls back to.regularMaterialonly on macOS 14/15. - No more glass-on-glass — the onboarding permission rows were glass cards sitting on what is now a glass window. Glass cannot sample other glass, so the rows are now plain content cards.
Note
The app has always been built against the macOS 26.1 SDK, so standard controls, the window chrome, sheets and menus already adopt Liquid Glass automatically. This release only corrects the two custom window backings.
Auto-update
If you are on v0.6.1+, this arrives automatically via Sparkle — the app offers v0.6.3 with an "Install and Relaunch" prompt.
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 190 Swift tests / 45 suites GREEN
- Apple notarization: DMG + zip both Accepted and stapled
- Onboarding + Settings windows now use real Liquid Glass — swapped the unconditional
v0.6.2 — Liquid Glass Settings + OTA verification
May 21, 2026View on GitHub →Completes the Liquid Glass pass and is the first build to verify the Sparkle auto-update path end-to-end (v0.6.1 → v0.6.2).
Changed
- Settings window adopts Liquid Glass — translucent window with the grouped
Form's opaque scroll backing hidden over anultraThinMateriallayer, matching the onboarding window. The macOS 26 System Settings look.
Auto-update
If you are on v0.6.1, this update arrives automatically — the app checks the appcast in the background and will offer v0.6.2 with an "Install and Relaunch" prompt. No manual download needed.
Install (first time)
Download the DMG below, drag into Applications, launch. Signed with Developer ID + Apple-notarized (DMG and update zip both stapled).
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 190 Swift tests / 45 suites GREEN
- Apple notarization: DMG + zip both Accepted and stapled
- Settings window adopts Liquid Glass — translucent window with the grouped
v0.6.1 — crash hotfix + draggable popups
May 21, 2026View on GitHub →Hotfix for v0.6.0, which crashed on launch. If you are on v0.6.0, download the DMG below manually — the crash happens before Sparkle starts, so auto-update can't reach you. Every release after this updates silently.
Fixed
- Crash on launch (
dyld: Library not loaded: @rpath/Sparkle.framework) —swift buildonly bakes rpaths for the.build/layout. Once the binary moved intoContents/MacOS/and Sparkle intoContents/Frameworks/, dyld had no rpath that resolved there. The packaging script now adds@executable_path/../Frameworksto the binary before signing.
Changed
- HUD + preview popups are draggable and resizable — drag from any non-control area to reposition, drag any edge to resize. The HUD keeps its position and size while a translation streams in.
- Onboarding window adopts Liquid Glass — translucent window with an
ultraThinMaterialbacking; the Accessibility / Input Monitoring permission rows are glass cards on macOS 26 Tahoe.
Install
- Download the DMG below, drag into Applications, launch.
- Signed with Developer ID + Apple-notarized (DMG and update zip both stapled).
Compatibility
- macOS 14 Sonoma minimum
- Liquid Glass effects on macOS 26 Tahoe
Tests
- 190 Swift tests / 45 suites GREEN
- Apple notarization: DMG Accepted, zip Accepted (
9649ce1f-…)
- Crash on launch (
v0.5.1 — Liquid Glass HUD + dismiss controls
May 20, 2026View on GitHub →Liquid Glass adoption + explicit dismiss controls for the result HUD. No more popups that feel stuck on screen.
What's new
- Liquid Glass background — On macOS 26 Tahoe the HUD and Preview panels use
.glassEffect(in:). Older systems get the existing.regularMateriallook as a graceful fallback. - X close button on the loading / result / error HUD — tap to dismiss instantly.
- Click anywhere outside the panel to dismiss — global mouse-down monitor watches for clicks in other apps and hides the HUD.
- Stable display preserved — the auto-hide timer (8s result, 6s error) is still the safety net. No focus-loss auto-dismiss that could surprise you mid-read.
Why
> "tôi nhớ AppKit của Mac mới nhất là Liquid design mà sao mở app lên không có. Với lại popup nên có nút tắt để user tắt sớm, hoặc bấm ra ngoài để tắt. Nhưng đảm bảo nó phải hiển thị ổn định."
Done meow~
Install
1. Download the DMG below 2. Open it, drag Contextual Mac Translator.app into Applications 3. Launch from Spotlight / Launchpad — signed + notarized by Apple, no right-click required
Compatibility
- macOS 14 Sonoma minimum (set in
Package.swift) - Liquid Glass effect kicks in on macOS 26 Tahoe; older systems get the
.regularMaterialfallback automatically
Tests
- 190 Swift tests / 45 suites GREEN
- Apple notarization: Accepted (submission
c6e5f957-…)
- Liquid Glass background — On macOS 26 Tahoe the HUD and Preview panels use
v0.5.0 — SwiftUI App + MenuBarExtra + Swift 6 mode
May 20, 2026View on GitHub →Architecture refactor: the app moves off the legacy
NSApplication.shared+ manualAppDelegatewiring and onto SwiftUI's modern@main App+MenuBarExtra. Compiles under Swift 6 language mode with strict concurrency checking.Changed
- MenuBarExtra refactor:
main.swift→ContextualMacTranslatorApp.swift.@main Appstruct with declarativeMenuBarExtrascene replaces the ~50-line imperativeNSStatusItem+NSMenusetup.AppDelegatelives on as@NSApplicationDelegateAdaptor, still owning the workflow, hotkey manager, HUDs, and the Settings + Onboarding windows. - Swift 6 language mode enabled on both executable and test targets. The codebase was already correctly annotated with
@MainActor,Sendablevalue types, and@Observablereference types, so the upgrade was a zero-diff for source files. - Dead code removed:
AppDelegate.buildStatusItemand thestatusItemproperty gone.
Install
Download the DMG, double-click to open, drag Contextual Mac Translator to Applications. Signed with Apple Developer ID and notarized — no Gatekeeper warning.
Notes
Tests: 190 Swift / 45 suites green. Apple Silicon, macOS 14+.
Full changelog: https://github.com/hoangperry/hp-translator/compare/v0.4.1...v0.5.0
- MenuBarExtra refactor:
v0.4.1 — Modern macOS UI: app icon + @Observable + symbol effects
May 20, 2026View on GitHub →UI modernization pass plus the long-missing app icon. Foundational bump to macOS 14 to unlock the modern SwiftUI surface.
Added
- App icon: hanko-stamp 訳 squircle (cream character on hanko-red background) matching the brand mark used across the marketing site. Finder, Dock, Spotlight, and Command-Tab all show the icon now.
- Symbol effects: HUD result icon bounces on new translation, error icon pulses, permission checkmarks bounce on grant; all status icons use hierarchical SF Symbol rendering.
Changed
- macOS minimum bumped to 14 (was 13). Unlocks \
@Observable\, \.symbolEffect\, modern \onChange\two-parameter form, and the rest of the macOS-14 SwiftUI surface. - State management → \`@Observable\`: all four observable classes (SettingsStore, PermissionManager, PreviewHUDViewModel, SupabaseAuthViewModel) use the Observation macro instead of ObservableObject + @Published. AppDelegate binding observation switched from Combine \
.sink\to \withObservationTracking\. - Onboarding polish: bigger Continue button (\
.borderedProminent\+ \.controlSize(.large)\), hierarchical info icons. - Preview HUD: edit pencil hierarchical + \
.controlSize(.large)\; deprecated \onChange(of:perform:)\updated to the macOS-14 closure form.
Install
Download the DMG, double-click to open, and drag Contextual Mac Translator to Applications. The build is signed with Apple Developer ID and notarized by Apple — no Gatekeeper warning, no right-click step.
Notes
Tests: 190 Swift / 45 suites green. Apple Silicon, macOS 14+.
Full changelog: https://github.com/hoangperry/hp-translator/compare/v0.4.0...v0.4.1
v0.4.0 — Cloud sign-in + device registration
May 19, 2026View on GitHub →First step onto the SaaS path: the app can now sign in to Contextual MT Cloud. Self-hosted and custom-backend modes are unchanged.
Added
- Contextual MT Cloud sign-in (opt-in): Settings → Translation Source → "Contextual MT backend" now offers a "Contextual MT Cloud · email sign-in" mode alongside the self-hosted token mode. Sign-in is a Supabase-backed email OTP (6-digit code, no password). _(M2.1-a)_
- Device registration: each Mac generates a device identity and sends
X-Device-*headers with cloud requests so the backend can enforce the per-plan device limit. _(M2.1-c)_ - Keigo-specialized style instruction: Japanese business outbound uses a keigo-tuned LLM instruction for more reliable 敬語 output.
- IT glossary starter pack: an EN/VI → JP IT-term preset for bridge engineers.
Changed
- Settings rebuilt as a grouped \
Form\for the native macOS System Settings appearance. - HUD and preview-panel borders now use the adaptive \
.separator\color (fixes an invisible border in light mode).
Install
Download the DMG, double-click to open, and drag Contextual Mac Translator to Applications. The build is signed with Apple Developer ID and notarized by Apple — no Gatekeeper warning, no right-click step. Bundle-ID migration and Sparkle auto-update remain planned for a later release.
Notes
Tests: 190 Swift / 45 suites green. Pre-release (alpha), Apple Silicon, macOS 13+.
Full changelog: https://github.com/hoangperry/hp-translator/compare/v0.3.1...v0.4.0
v0.3.1 — Hotkey recorder + custom style + server DeepL/LibreTranslate mirror
May 11, 2026View on GitHub →What's new in v0.3.1
Closes the v0.3 follow-up backlog.
Customizable hotkey recorder
- Click "Change…" next to any hotkey in Settings → press your new combo → save.
- Built on
NSEvent.keyDownwith proper Cocoa-flag-to-Carbon-modifier-mask mapping. - Esc cancels mid-recording.
- Validates: at least one modifier (⌘/⌥/⌃/⇧) + a regular key.
Hotkey conflict detection
- Recorder warns inline when the pressed combo is already bound to another translation target (or to the inbound hotkey).
- Save button disables until the conflict is resolved.
Custom LLM style instruction per binding
- Expandable "Custom style instruction" field on each outbound binding.
- Overrides the auto-derived (language, register) style — useful when you want very specific tone control for a single target.
- Empty = uses the default register-aware instruction (back-compat).
Server-side mirror (translator-server)
DeepLProvider— backend mode now supports DeepL Free/Pro with the sameformality=more/lessmapping as the app.LibreTranslateProvider— backend can self-host or proxy to a LibreTranslate instance.- Aliases
libre/libre-translate/libre_translateall dispatch tolibretranslate. - 16 new pytest cases. Total server tests: 68 → 84 GREEN.
Tests
- 150 Swift / 35 suites + 84 pytest / 17 classes = 234 GREEN.
Docs
CHANGELOG.mdadded with version history v0.1.0 → v0.3.1.docs/ENV.md+docs/api-contract.mddocument the new providers + aliases.
Upgrade
- Existing v0.3.0 installs migrate transparently; the new
customStyleInstructionfield onOutboundBindingdefaults to empty (= use derived style). - Apple Silicon only (arm64). Ad-hoc signed.
Install
1. Download
Contextual-Mac-Translator-v0.3.1-macos-arm64.dmg. 2. Open DMG → drag app to /Applications. 3. Launch from /Applications, grant Accessibility. 4. Open Settings → Languages, change hotkeys via "Change…", customize style per target.Full changelog
See [CHANGELOG.md](https://github.com/hoangperry/hp-translator/blob/main/CHANGELOG.md).
v0.3.0 — Multi-language + DeepL + LibreTranslate + Customizable Hotkeys
May 10, 2026View on GitHub →What's new in v0.3.0
Multi-language support
- Pick any language pair. Replaced the hardcoded VI ↔ JP setup with
TranslationStyle(direction + BCP47 target language + register). 30 curated languages in Settings dropdown (vi/ja/en/ko/zh/fr/de/es/...). - Persona refactor:
formalregister maps to keigo (JP), jondaemal (KR), vouvoiement (FR), polite English, etc. — the LLM picks per-language conventions from one sharedRegisterenum. - My language picker in Settings: every inbound translation targets it; outbound uses it as source.
Customizable hotkey bindings
- Unlimited outbound targets. Each (target language + register) pair gets its own configurable hotkey.
- Default v0.2 layout preserved:
⌘⏎= Japanese formal,⌥⏎= Japanese casual,⌥D= inbound to Vietnamese. - Add/remove targets in Settings → Languages → Outbound translations. Hotkeys re-register automatically when you save.
2 new free providers
- DeepL Free — top NMT quality for European + East Asian languages, free tier 500K chars/month.
formality=more/lessmapped to formal/casual register. - LibreTranslate — open-source MT, self-host via Docker for unlimited private use, or use the community instance.
App now ships 9 direct providers total: Gemini · Ollama · DeepL · LibreTranslate · Google Translate · OpenAI-compatible · Gemini CLI · Codex CLI · Mock.
v0.2.1 bug fixes bundled
- Fixed double-HUD when preview opens (loading HUD now dismisses).
- Fixed
Cmd-Vnot working in Settings text fields (added Edit menu). - Fixed Codex/Gemini CLI not found from
/Applications(extended PATH with/opt/homebrew/bin).
Tests
- 150 Swift tests / 35 suites + 68 pytest = 218 GREEN (+28 from v0.2.0).
- New coverage for DeepL, LibreTranslate, multi-binding persistence, hotkey display.
Upgrade notes
- Existing v0.2 users: settings auto-migrate to vi/ja default with the same hotkeys. Open Settings → Languages to add more outbound targets or change your primary language.
- Apple Silicon only (arm64). Ad-hoc signed; first launch may need right-click → Open.
Install
1. Download
Contextual-Mac-Translator-v0.3.0-macos-arm64.dmg. 2. Open DMG → drag app to /Applications. 3. Launch from /Applications, grant Accessibility. 4. Open Settings, configure languages + provider, test hotkey.- Pick any language pair. Replaced the hardcoded VI ↔ JP setup with
v0.2.0 — Multi-provider + Streaming SSE
May 9, 2026View on GitHub →What's new in v0.2.0
App-side multi-provider support (Phase 3)
- New Translation Source picker: Direct API / Custom backend / 1st-party backend
- 7 direct providers — call APIs without a backend hop:
- Gemini (Google AI Studio) - Ollama (local, privacy-first) - Google Translate Basic - OpenAI-compatible (LM Studio / OpenRouter / vLLM / OpenAI) - Gemini CLI (experimental) - Codex CLI (experimental) - Mock (echo, for smoke tests)
- Per-provider credential slots in macOS Keychain
- TranslationProviderFactory dispatches based on Settings; switch sources without re-entering keys
Streaming SSE (Phase 5)
- New
/translate/streamendpoint emits SSE chunks - Inbound flow shows progressive HUD as tokens arrive
- Mid-stream errors surface as RFC 7807 frames
v2 client adoption (Phase 2.5)
- Idempotency-Key on every request → server cache prevents double-paste
- RFC 7807 problem-details parsing → richer error messages
- 429 + Retry-After UX → \"slow down, retry in 3s\" instead of generic error
Production polish (Phase 7)
- X-Forwarded-For aware rate limiting (opt-in via env)
- DEPLOY guide with monitoring, troubleshooting, SSE nginx config
Tests
- 122 Swift / 29 suites + 68 pytest / 14 classes = 190 GREEN
Install (alpha)
1. Download \
Contextual-Mac-Translator-v0.2.0-macos-arm64.dmg\2. Open DMG → drag app to /Applications 3. Launch from /Applications 4. Grant Accessibility (System Settings → Privacy & Security) 5. Open Settings, pick a Source, paste credentials, test hotkeyNotes
- Apple Silicon only (arm64)
- Ad-hoc signed; first launch may need right-click → Open
- Bundle ID still placeholder (\
app.lookerlab.translator\); Developer ID signing is on the M2 roadmap
v0.1.5 alpha DMG
May 8, 2026View on GitHub →Alpha macOS DMG for local testing. This build adds an onboarding fallback for macOS TCC lag: if Accessibility is already enabled in System Settings but the app status has not refreshed, use Continue Anyway, then quit/reopen if hotkeys do not respond. DMG includes Contextual Mac Translator.app and an Applications symlink. SHA256: acdb48f21ee9bc94e3eb66a624dcad900513b23106a732757b62a45a3c7d7575
v0.1.4 alpha DMG
May 8, 2026View on GitHub →Alpha macOS DMG for local testing. This build improves the permissions onboarding screen: Accessibility is clearly marked required, Input Monitoring is optional/recommended, Continue only requires Accessibility, and the screen includes guidance for manually adding /Applications/Contextual Mac Translator.app when macOS does not list it automatically. SHA256: 0c67407aa6ea8926fc02b1adc1738ba638c0235871e6b871c50512c480ba53fc
v0.1.3 alpha DMG
May 8, 2026View on GitHub →Alpha macOS DMG for local testing. Open the DMG and drag Contextual Mac Translator.app into Applications. The app bundle is ad-hoc signed, but the DMG/app are not Developer ID signed or notarized, so macOS may require right-click Open on first launch. SHA256: 9b191ce9bb1b67b0133f74f4ea08282b790a1047bd9ad0f896494e8ed7a4d4f8
v0.1.2 alpha installer
May 8, 2026View on GitHub →Alpha macOS installer package for local testing. The package installs Contextual Mac Translator.app into /Applications. The app bundle is ad-hoc signed, but the installer package itself is not Developer ID signed/notarized, so macOS may require right-click Open or command-line install. SHA256: 824310dd726e26ab58a8201f2adfe598a97b9824550fdaa9631a910bf8467237
v0.1.1 alpha
May 8, 2026View on GitHub →Alpha macOS app build for local testing. This build ad-hoc signs the full .app bundle so macOS TCC can identify it as app.lookerlab.translator / Contextual Mac Translator. Still unsigned by Developer ID and not notarized, so macOS may require right-click Open on first launch. Move the app to /Applications before approving Accessibility/Input Monitoring. SHA256: db27c866b44c3324a79318b3eae494529e6a9747a3610b623e3a08ee42555ae0
v0.1.0 alpha
May 8, 2026View on GitHub →Alpha macOS app build for local testing. This bundle is unsigned/not notarized, so macOS may require right-click Open on first launch. SHA256: 9852561bc7f6717eb95af2c5aab7fad40219b767504e4ff89cfc0637620cdf15