v0.10.0 — Cultural Precision & Privacy (VN Register + Privacy badge + Glossary v2)
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)
RegisterCardwith 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
ProviderPrivacyClassenum (.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)
GlossaryEntryCodable 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. GlossaryComposerpure 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 between TranslationWorkflow + 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