Add preset-driven polish scenarios (Settings, onboarding, ScenarioChip)
with ScenarioPrompt and style directives; drive keyboard height from
content (240pt) and use viewIsAppearing encapsulated-height offset for
smoother keyboard switches; remove redundant StatusBadge and hide system
dictation via hasDictationKey.
- Add LLMProvider.isUserSelectable (default true) and filter
ProviderPickerSection on it; next pass can hide non-user presets
(e.g. a future DeepSeek key-preset) without changing call sites.
- KeyboardRootView: hide TranslationChip when off (matches user's
mental model of an opt-in feature), drop the 'warming' branch
(Qwen3 download UX was removed with the backend in v0.2.0), unify
chip pill height to minHeight 28 + vertical 6 for visual rhythm
across all topbar chips.
- TranslationChip: drop isLocal warning path — both engines now
run the translate-and-polish step (local routes through DeepSeek
via ProviderConfig.localModeProviderId).
- OnboardingView: cloud engine branch now wraps the translation
row in the same surface card chrome as the local branch.
- Strings: drop keyboard.models.warming (no longer referenced).
DeepSeek key pre-fill deferred to a follow-up.
Adds an opt-in translation pipeline that reuses the existing
PolishingService + LLMClient + AppGroupStore chain. Translation is
implemented as a new PolishMode (.translate(targetLocaleId:)); all
existing call sites are unchanged.
Settings:
- New TranslationPickerRow in the language tab (Toggle + 10-locale
picker: en/zh-Hans/zh-Hant/ja/ko/fr/de/es/ru/pt), persisted to the
App Group so the keyboard extension can read it during live dictation.
- 5 new strings per language (en + zh-Hans).
Keyboard:
- New TranslationChip on the top bar to the right of LocaleChip;
same Menu pattern, lets users toggle or quickly switch target
language without leaving the keyboard.
- PolishingService dispatches .translate with a parameterised prompt
(en/zh variants selected by provider id); PolishingService.error
gains a translationNotAvailable case so local-engine users get a
clear inline warning when the toggle is on but cloud is off.
- 6 new strings per language (en + zh-Hans) for the chip + banner.
Local engine policy:
- Translation is cloud-only by design (local engine stays ASR-only
to honour the no-roundtrip promise). Chip shows a 'cloud required'
state and raw transcript still inserts on failure — no data loss.
Build:
- OSGKeyboardShared adds TranslationLanguage enum (10 locales) and
TranslationPrompt factory.
- 4 new files, 9 modified. xcodebuild scheme=OSGKeyboard
config=Debug destination=iPhone 17 Simulator: BUILD SUCCEEDED
(0 warning, 0 error).
Also pins DEVELOPMENT_TEAM in project.yml for TestFlight uploads
(3 targets; Team X329MZU23S).
Replace MLX GPU inference with CoreML bundles so transcription continues
while the host app is backgrounded. Adds model download and warm-up,
vendored Qwen3Speech, and updates onboarding, settings, and copy for the
~1.6 GB CoreML package (iOS 18+).
Streamline the five-step flow with smarter skip logic, a centered welcome intro, clearer zh copy, keyboard setup detection via the extension, and home tips when permissions are still missing after onboarding completes.
Renew voice sessions while the host app stays foreground, auto-start flow from the keyboard with a Start action, and prevent leading audio loss via pre-roll buffering and faster signal polling.
XcodeGen merged Localizable.strings into the main app only, so the keyboard
extension had no .lproj files and NSLocalizedString returned raw keys. Use a
dedicated Keyboard.strings table and explicit buildPhase: resources entries.
Remove KeyboardL10n hard-coded fallbacks in favor of ExtL10n and extension
Localizable.strings. Add Flow session expiry hints, Darwin cross-process
notifications, session monitor on the keyboard, and app icon on GitHub Pages.
Migrate keyboard dictation to continuous Flow sessions with auto-start,
tap-to-toggle recording, 60s countdown, five-step onboarding, and App Group
IPC. Add docs/ GitHub Pages site with en/zh privacy policy for App Store compliance.
1) Preview chips weren't actually buttons.
`modeChip` and `localeChip` in `KeyboardPreviewStub` were
decorative HStacks — no `Button`, no action, no callback. The
chevron-down glyph made them *look* like pickers, so a user
tapping them got nothing. The screenshot the user shared
("润色 ▾" / "中文(简体) ▾") shows exactly that surface.
Fix: wrap each chip in a `Button(action: ...)` with
`.buttonStyle(.plain)`. The stub now takes `modeId`, `localeId`,
`onModeCycle`, `onLocaleCycle` and the sheet's `cycleMode` /
`cycleLocale` advance the config:
- mode cycles [off → transcribe → polish] (mirrors Settings)
- locale cycles [auto → zh-Hans → zh-Hant → en-US → ja-JP → ko-KR]
Mid-recording locale switches call `asr.stop()` because ASR
sessions are bound to the locale they were started with.
`modeId == "off"` also stops any in-flight recording so the
disc isn't recording into a mode that won't insert.
The mode chip's icon also follows the mode (mic.slash /
mic / wand) as a redundant visual cue, and both chips get
accessibility labels (preview.modeChip.cycle /
preview.localeChip.cycle) so VoiceOver users can use them.
2) Onboarding's "Next" stays enabled when local engine is picked
but no API key is filled in. Root cause: `ProviderConfig.isConfigured`
checks `!apiKey.isEmpty && !baseURL.isEmpty && !model.isEmpty` —
it never asks whether the user *needs* a key. The local engine
(on-device ASR) doesn't round-trip through the LLM, so an
empty key on the local path is correct, not a configuration gap.
Fix: short-circuit `isConfigured` to `true` when
`engineMode == "local"`. The onboarding "Next" button is
already disabled on the API page when `!isConfigured`; this
just makes the gate respect the engine choice. New test
`testIsConfiguredTrueForLocalEngineWithoutAPIKey` locks the
behaviour in (local → true, cloud → false, flip back).
3) Add the two new accessibility keys to all four
`Localizable.strings` files (en + zh-Hans, main app + ext)
so VoiceOver and the cycle button labels resolve in both
languages.
Build: BUILD SUCCEEDED.
Tests: 22/22 pass (1 new).
🤖 Generated with Claude Code
Five user-flagged issues addressed in this commit. Some of the
uncommitted files belong to a prior agent pass and are included as-is
so this is a clean working tree.
1. Remove globe (nextKeyboard) button from keyboard bottom bar.
iOS already provides a globe key in the system keyboard strip
for next-keyboard switching, so the in-extension one was
redundant. The bar is now: ⌫ (delete) [space] ↩ (return).
Affected: OSGKeyboardExt/Views/KeyboardRootView.swift and
OSGKeyboard/Views/KeyboardPreviewStub.swift (preview mirrors).
2. Fix TabView with .page style auto-jumping on TextField focus.
SwiftUI's `.tabViewStyle(.page(...))` wraps content in a
UIPageViewController, which has a long-standing iOS 18 bug
where the keyboard-showing layout reflow on a TextField focus
is misread as a horizontal swipe — the page jumps back to
step 1 the moment the user starts typing. Replaced the
TabView with a ZStack + conditional view + transition. We give
up swipe-to-page, but Back/Next buttons + page dots are the
canonical onboarding affordance and the user is one tap from
the next page anyway.
3. Onboarding APISetupPage now offers Engine choice (Local vs
Cloud), matching the in-app Settings page. First-run users can
pick the on-device engine and skip the API-key setup entirely.
Extracted the engine section from SettingsView into a shared
`EnginePickerSection` component used by both. Cloud path
keeps the provider + API fields; Local path shows a
"no API key needed" confirmation card.
4. APISettingsCard test-connection error messages are now built
with NSLocalizedString + String.localizedStringWithFormat (for
the interpolated HTTP status / reply preview). Status badge
labels are also NSLocalizedString-backed.
5. iOS-standard localization.
- New `en.lproj/Localizable.strings` and `zh-Hans.lproj/
Localizable.strings` (in both the main app and the keyboard
extension bundles — each .appex has its own bundle).
- `project.yml` sets `CFBundleDevelopmentRegion: en` and
`CFBundleLocalizations: [en, zh-Hans]`; the two .lproj dirs
are added as resources.
- Every `Text("...")` / `Button("...")` / `Label("...")` /
`accessibilityLabel(Text("..."))` in user-facing views was
rewritten to use `Text("key")` (SwiftUI auto-resolves
string-literal `LocalizedStringKey`s against the strings
file) or `NSLocalizedString("key", comment: "")` for
interpolated / dynamic values. The hardcoded
"中 · EN" / "EN · 中" pattern is gone.
- Two helper signatures that took `String` for the title/body
of a row (`footnoteRow`, `sectionHeader`) are now
`LocalizedStringKey` so the row labels are looked up.
- `Label("key", systemImage: "...")` and
`.confirmationDialog(LocalizedStringKey("key"), ...)` and
`.navigationTitle(LocalizedStringKey("key"))` are used where
`String` would just print the key.
- Preview-onboarding `APISetupPage` updated to use the same
engine picker as Settings (issue 3), with a section that
only renders the provider + API fields when the user picks
Cloud.
Verified on iOS 26 simulator with system language set to both
zh-Hans (default) and en: the same screen renders "按住说话,松开
即得润色文字。" / "下一步" in Chinese, and "Hold to talk. Release
for polished text, in any app." / "Next" in English — no
"中 · EN" doubling, no missing keys.
Build: BUILD SUCCEEDED.
Tests: 21/21 pass.
🤖 Generated with Claude Code