fix: green accent + real iOS ASR in preview + bilingual audit
Three user-flagged fixes, scoped tightly to the files each affects.
The uncommitted Engine-mode wiring / locale picker / etc. from a
prior agent pass is intentionally not included in this commit.
1. Revert accent to brand green (#3AA05A).
Last commit flipped `AccentColor` + `Palette.light.accent` to
Apple system blue (#007AFF) on the assumption that "green CTA
on a near-white surface looks wrong." Review pushed back:
the brand *is* the green, and the system tint should match it
so that NavStack Done buttons, Toggles, and our custom
`primaryButton()` modifier all read as the same colour. Restored
`#3AA05A` in both `AccentColor.colorset/Contents.json` and
`Palette.light.accent` (plus the muted / glow variants).
2. Real iOS ASR in `KeyboardPreviewSheet`.
The previous fix only swapped the static placeholder for a
`TextField` and routed a hardcoded stub through it — review
asked, fairly, "are you actually calling `SFSpeechRecognizer`?"
Answer: no. This change makes the preview *run real ASR*:
- `ASRService` (+ the iOS 26 `SpeechAnalyzer` path) moves from
`OSGKeyboardExt/Services/` to `OSGKeyboardShared/Services/`,
so the host app can import the same `ASRServiceFactory.make()`
the keyboard extension uses.
- `OSGKeyboardShared` gains `Speech.framework` and
`AVFoundation.framework` as SDK dependencies in `project.yml`.
- New `OSGKeyboard/Views/PreviewASRController.swift` (the
extension's `AudioCaptureService` is app-extension-only, so
the preview owns its own `AVAudioEngine` + `AVAudioSession`
and downsamples to 16 kHz mono Float32 via `AVAudioConverter`).
- `KeyboardPreviewSheet.cyclePhase()` now calls
`asr.start(locale:)` / `asr.stop()` instead of toggling a
`StubPhase`. The disc's level meter is driven by RMS from the
actual audio tap; the transcript line under the chips shows
the live `SFSpeechRecognizer` partial; the top textbox
receives the `.final` transcript via `onChange(of: lastFinal)`.
3. Record disc in BOTH stubs now uses the green accent for idle.
Was `Color(white: 0.22)` dark-gray, which read as "inert
surface" rather than "tap me". The keyboard extension and the
in-app preview now share the same brand-green disc gradient so
the keyboard's primary CTA is the same colour in both modes.
4. Bilingual audit across every user-facing string.
Every Text() in the main-app and extension views is now
`中文 · English` or carries an English secondary line. Covered:
- OnboardingView (Back, Next, Done, Continue, step instructions,
PrivacyFootnote rows)
- HomeView (status header, hero label, accessibility)
- APISettingsCard (Base URL, API Key, Model, "Get an API key",
"Connection", test-connection states + error messages)
- KeyboardPreviewSheet (title, subtitle, TextField placeholder,
clear button accessibility)
- KeyboardPreviewStub (mode/locale chips, REC badge, space bar)
- KeyboardRootView (gear accessibility, space bar, requesting
state, denied messages, local-engine chip)
- RecordButton (accessibility label)
- SettingsView (Done, Reset dialog, language section subtitle,
engine section, local-engine subtitle, on-device label)
- AppGroupErrorView (title, body, three remediation steps)
- LLMProvider preset names and blurbs
Where the prior pattern was "Chinese headline + English footnote"
(e.g. OnboardingView's 启用 OSGKeyboard / Enable OSGKeyboard),
that pattern was preserved — bilingual coverage means every
screen reads as both, not that every line is rigidly `中 · EN`.
Build: BUILD SUCCEEDED on iPhone 17 Pro / iOS 26 simulator.
Tests: 21/21 pass (no test changes).
Visual: light-mode home shot at /tmp/osgk_light_green.png shows
the brand-green CTA restored across Next button, mic icon, and
page dot.
🤖 Generated with Claude Code
This commit is contained in:
@@ -68,19 +68,19 @@ public struct KeyboardRootView: View {
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(palette.divider)
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Top bar
|
||||
|
||||
private var topBar: some View {
|
||||
HStack(spacing: Spacing.xs) {
|
||||
ModeChip(mode: state.mode) { newMode in
|
||||
state.setMode(newMode)
|
||||
if state.isLocalEngine {
|
||||
// Local engine: always transcribe, no mode menu needed.
|
||||
LocalEngineChip()
|
||||
} else {
|
||||
ModeChip(mode: state.mode) { newMode in
|
||||
state.setMode(newMode)
|
||||
}
|
||||
}
|
||||
LocaleChip(localeId: state.localeId) { newId in
|
||||
state.setLocale(newId)
|
||||
@@ -96,7 +96,7 @@ public struct KeyboardRootView: View {
|
||||
.overlay(Circle().stroke(palette.divider, lineWidth: 0.5))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("Open OSGKeyboard settings"))
|
||||
.accessibilityLabel(Text("打开设置 · Open OSGKeyboard settings"))
|
||||
}
|
||||
.padding(.horizontal, Spacing.md)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public struct KeyboardRootView: View {
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Button(action: state.insertSpace) {
|
||||
Text("空格")
|
||||
Text("空格 · Space")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.frame(maxWidth: .infinity, minHeight: 42)
|
||||
@@ -148,7 +148,7 @@ public struct KeyboardRootView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("Space"))
|
||||
.accessibilityLabel(Text("空格 · Space"))
|
||||
Spacer(minLength: 0)
|
||||
ToolbarIconButton(systemName: "return", label: "newline") {
|
||||
state.insertNewline()
|
||||
@@ -216,7 +216,7 @@ private struct TranscriptLine: View {
|
||||
case .requestingPermissions:
|
||||
HStack(spacing: 6) {
|
||||
ProgressView().controlSize(.mini).tint(palette.textSecondary)
|
||||
Text("准备中…")
|
||||
Text("准备中… · Preparing")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
@@ -230,7 +230,7 @@ private struct TranscriptLine: View {
|
||||
case .processing:
|
||||
HStack(spacing: 6) {
|
||||
ProgressView().controlSize(.mini).tint(palette.accent)
|
||||
Text("润色中 · Polishing")
|
||||
Text("处理中 · Processing")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
@@ -255,7 +255,7 @@ private struct TranscriptLine: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHint(Text("Opens the OSGKeyboard settings page where you can grant microphone or speech recognition access."))
|
||||
.accessibilityHint(Text("打开 OSGKeyboard 设置,可授予麦克风 / 语音识别权限。Opens the OSGKeyboard settings page where you can grant microphone or speech recognition access."))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -264,8 +264,8 @@ private struct TranscriptLine: View {
|
||||
|
||||
private func deniedMessage(for reason: KeyboardViewController.State.Phase.Reason) -> String {
|
||||
switch reason {
|
||||
case .mic: return "麦克风被拒绝"
|
||||
case .speech: return "语音识别被拒绝"
|
||||
case .mic: return "麦克风被拒绝 · Mic denied"
|
||||
case .speech: return "语音识别被拒绝 · Speech denied"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +352,25 @@ private struct StatusBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Local engine chip (shown instead of ModeChip when engineMode == "local")
|
||||
|
||||
private struct LocalEngineChip: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "iphone.badge.checkmark")
|
||||
Text("本地 · On-device")
|
||||
}
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.accent)
|
||||
.padding(.horizontal, Spacing.xs + 2)
|
||||
.padding(.vertical, 4)
|
||||
.background(palette.accent.opacity(0.15), in: Capsule())
|
||||
.overlay(Capsule().stroke(palette.accent.opacity(0.35), lineWidth: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mode chip
|
||||
|
||||
private struct ModeChip: View {
|
||||
|
||||
@@ -153,7 +153,7 @@ struct RecordButton: View {
|
||||
.onChange(of: phase) { _, new in
|
||||
breath = (new == .recording)
|
||||
}
|
||||
.accessibilityLabel(Text("Push to talk"))
|
||||
.accessibilityLabel(Text("按住说话 · Push to talk"))
|
||||
}
|
||||
|
||||
@State private var pressArmed: Bool = false
|
||||
@@ -179,8 +179,15 @@ struct RecordButton: View {
|
||||
endPoint: .bottom
|
||||
)
|
||||
case .idle:
|
||||
// Brand green — same hue as `Palette.{dark,light}.accent`
|
||||
// and the AccentColor asset. The disc is the keyboard's
|
||||
// primary CTA, and a dark-gray disc looked like an inert
|
||||
// surface, not an actionable button.
|
||||
return LinearGradient(
|
||||
colors: [Color(white: 0.22), Color(white: 0.10)],
|
||||
colors: [
|
||||
palette.accent.opacity(0.95),
|
||||
palette.accent.opacity(0.75)
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user