feat: complete Phase 4 batch F, keyboard i18n, and Pages app icon
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.
This commit is contained in:
@@ -62,6 +62,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
private var flowWatchdogTask: Task<Void, Never>?
|
||||
private var utteranceTimerTask: Task<Void, Never>?
|
||||
private var utteranceStartedAt: TimeInterval = 0
|
||||
private var wasFlowSessionActive = false
|
||||
private var flowSessionMonitorTask: Task<Void, Never>?
|
||||
private var flowSessionDarwinObserver: FlowSessionDarwinObserver?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
@@ -76,17 +79,22 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
loadPersistedConfig()
|
||||
consumePendingDictationResultIfNeeded()
|
||||
refreshDictationProgressStateIfNeeded()
|
||||
installFlowSessionDarwinObserver()
|
||||
refreshFlowSessionState()
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
cancelPipeline()
|
||||
stopFlowSessionMonitor()
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
consumePendingDictationResultIfNeeded()
|
||||
refreshDictationProgressStateIfNeeded()
|
||||
refreshFlowSessionState()
|
||||
startFlowSessionMonitor()
|
||||
}
|
||||
|
||||
public override func didReceiveMemoryWarning() {
|
||||
@@ -142,10 +150,57 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
case .loaded:
|
||||
break
|
||||
case .unavailable:
|
||||
state.phase = .error(.appGroupUnavailable, message: "App Group 未配置")
|
||||
state.phase = .error(
|
||||
.appGroupUnavailable,
|
||||
message: ExtL10n.string("keyboard.error.appGroupUnavailable")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Flow session monitor
|
||||
|
||||
private func installFlowSessionDarwinObserver() {
|
||||
flowSessionDarwinObserver = FlowSessionDarwinObserver { [weak self] in
|
||||
self?.refreshFlowSessionState()
|
||||
}
|
||||
}
|
||||
|
||||
private func startFlowSessionMonitor() {
|
||||
flowSessionMonitorTask?.cancel()
|
||||
flowSessionMonitorTask = Task { @MainActor [weak self] in
|
||||
while !Task.isCancelled {
|
||||
self?.refreshFlowSessionState()
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopFlowSessionMonitor() {
|
||||
flowSessionMonitorTask?.cancel()
|
||||
flowSessionMonitorTask = nil
|
||||
}
|
||||
|
||||
private func refreshFlowSessionState() {
|
||||
let active = FlowSessionBridge.isSessionActive()
|
||||
state.flowSessionActive = active
|
||||
|
||||
if wasFlowSessionActive && !active && !isFlowRecording && !isPendingFlowStart {
|
||||
switch state.phase {
|
||||
case .recording, .processing:
|
||||
break
|
||||
default:
|
||||
showFlowSessionExpiredHint()
|
||||
}
|
||||
}
|
||||
wasFlowSessionActive = active
|
||||
}
|
||||
|
||||
private func showFlowSessionExpiredHint() {
|
||||
let message = ExtL10n.string("keyboard.flow.sessionExpired")
|
||||
state.phase = .error(.unknown(message), message: message)
|
||||
scheduleAutoClearError()
|
||||
}
|
||||
|
||||
// MARK: - Press handlers
|
||||
|
||||
private func toggleRecording() {
|
||||
@@ -168,13 +223,13 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
}
|
||||
guard state.mode != .off else { return }
|
||||
guard hasFullAccess else {
|
||||
let msg = "请在系统设置中为 OSGKeyboard 开启“允许完全访问”,否则无法使用语音输入"
|
||||
let msg = ExtL10n.string("keyboard.error.fullAccessRequired")
|
||||
state.phase = .error(.unknown(msg), message: msg)
|
||||
scheduleAutoClearError()
|
||||
return
|
||||
}
|
||||
guard AppGroup.isAvailable else {
|
||||
let msg = "App Group 未配置,键盘无法与主 App 通信。请重新安装并检查签名配置。"
|
||||
let msg = ExtL10n.string("keyboard.error.appGroupCommunication")
|
||||
state.phase = .error(.appGroupUnavailable, message: msg)
|
||||
scheduleAutoClearError()
|
||||
return
|
||||
@@ -198,7 +253,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
stopUtteranceCountdown()
|
||||
FlowSessionBridge.setRecordingState(.stopped)
|
||||
state.phase = .processing
|
||||
state.lastTranscript = "识别中..."
|
||||
state.lastTranscript = ExtL10n.string("keyboard.flow.transcribing")
|
||||
startFlowResultWatchdog()
|
||||
}
|
||||
|
||||
@@ -245,7 +300,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
isPendingFlowStart = true
|
||||
isFlowRecording = false
|
||||
flowStartDeadline = Date().timeIntervalSince1970 + FlowWatchdog.startTimeout
|
||||
state.lastTranscript = "正在启动语音会话..."
|
||||
state.lastTranscript = ExtL10n.string("keyboard.flow.startingSession")
|
||||
state.phase = .processing
|
||||
openHostApp(path: "startflow")
|
||||
startFlowStartWatchdog()
|
||||
@@ -312,7 +367,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
let now = Date().timeIntervalSince1970
|
||||
if now - startedAt > FlowWatchdog.resultTimeout {
|
||||
self.stopFlowWatchdog()
|
||||
let msg = "等待识别结果超时,请重试"
|
||||
let msg = ExtL10n.string("keyboard.flow.resultTimeout")
|
||||
self.state.phase = .error(.unknown(msg), message: msg)
|
||||
self.scheduleAutoClearError()
|
||||
return
|
||||
@@ -396,13 +451,13 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
// Don't silently insert the raw transcript — the user
|
||||
// thinks they're getting polished text when really no
|
||||
// key is configured. Show a precise, actionable error.
|
||||
self.state.phase = .error(.llm(error), message: "未配置 API Key · 请在主 App 设置中填写")
|
||||
self.state.phase = .error(.llm(error), message: ExtL10n.string("keyboard.error.llm.noApiKey"))
|
||||
self.scheduleAutoClearError()
|
||||
case .http(401):
|
||||
self.state.phase = .error(.llm(error), message: "API Key 无效 (401) · 请检查主 App 设置")
|
||||
self.state.phase = .error(.llm(error), message: ExtL10n.string("keyboard.error.llm.unauthorized"))
|
||||
self.scheduleAutoClearError()
|
||||
case .http(429), .rateLimited:
|
||||
self.state.phase = .error(.llm(error), message: "API 限流 (429) · 请稍后再试")
|
||||
self.state.phase = .error(.llm(error), message: ExtL10n.string("keyboard.error.llm.rateLimited"))
|
||||
self.scheduleAutoClearError()
|
||||
case .cancelled:
|
||||
// User-initiated cancellation (e.g. mode switch mid-
|
||||
@@ -464,7 +519,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
|
||||
private func openHostApp(path: String = "settings") {
|
||||
guard hasFullAccess else {
|
||||
let msg = "未开启“允许完全访问”,请先在键盘设置中打开"
|
||||
let msg = ExtL10n.string("keyboard.error.fullAccessForJump")
|
||||
state.phase = .error(.unknown(msg), message: msg)
|
||||
scheduleAutoClearError()
|
||||
return
|
||||
@@ -485,7 +540,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
// Flow start: auto-jump often fails in WeChat/Safari — keep polling
|
||||
// so a manually opened host app can still satisfy the session check.
|
||||
if path == "startflow", isPendingFlowStart {
|
||||
state.lastTranscript = "无法自动跳转,请从主屏幕打开 OSGKeyboard,然后返回继续"
|
||||
state.lastTranscript = ExtL10n.string("keyboard.flow.manualOpenHost")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -508,18 +563,18 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
switch progress.status {
|
||||
case .requested:
|
||||
state.lastTranscript = state.isLocalEngine
|
||||
? "正在打开 OSGKeyboard(本地转写)..."
|
||||
: "正在打开 OSGKeyboard..."
|
||||
? ExtL10n.string("keyboard.dictation.openingLocal")
|
||||
: ExtL10n.string("keyboard.dictation.opening")
|
||||
case .recording:
|
||||
state.lastTranscript = state.isLocalEngine
|
||||
? "正在本地录音,请完成后返回当前输入页"
|
||||
: "正在录音,请完成后返回当前输入页"
|
||||
? ExtL10n.string("keyboard.dictation.recordingLocal")
|
||||
: ExtL10n.string("keyboard.dictation.recording")
|
||||
case .transcribing:
|
||||
state.lastTranscript = state.isLocalEngine
|
||||
? "本地识别中,请稍候并返回输入页"
|
||||
: "识别中,请稍候并返回输入页"
|
||||
? ExtL10n.string("keyboard.dictation.transcribingLocal")
|
||||
: ExtL10n.string("keyboard.dictation.transcribing")
|
||||
case .error:
|
||||
let msg = progress.message ?? "录音失败,请重试"
|
||||
let msg = progress.message ?? ExtL10n.string("keyboard.dictation.failed")
|
||||
debug("host returned error: \(msg)")
|
||||
awaitingDictationResult = false
|
||||
stopDictationWatchdog()
|
||||
@@ -538,7 +593,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
let now = Date().timeIntervalSince1970
|
||||
let lastProgressAt = progress.updatedAt > 0 ? progress.updatedAt : dictationRequestStartedAt
|
||||
if now - lastProgressAt > DictationWatchdog.timeout {
|
||||
let timeoutMessage = "等待录音结果超时,请返回 OSGKeyboard 完成录音后重试"
|
||||
let timeoutMessage = ExtL10n.string("keyboard.dictation.resultTimeout")
|
||||
debug("dictation timeout after \(Int(now - lastProgressAt))s")
|
||||
awaitingDictationResult = false
|
||||
stopDictationWatchdog()
|
||||
@@ -551,15 +606,15 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
private func showManualSettingsHint(path: String = "settings") {
|
||||
let msg: String
|
||||
if !hasFullAccess {
|
||||
msg = "请先开启 OSGKeyboard 的“允许完全访问”,否则键盘无法跳转到 App"
|
||||
msg = ExtL10n.string("keyboard.error.fullAccessForJump")
|
||||
} else if path == "settings" {
|
||||
msg = "系统拒绝了键盘跳转。请手动打开 OSGKeyboard App 进入设置页"
|
||||
msg = ExtL10n.string("keyboard.error.manualOpenSettings")
|
||||
} else if path == "startflow" {
|
||||
msg = "语音会话未启动。请从主屏幕打开 OSGKeyboard App,返回后再按麦克风"
|
||||
msg = ExtL10n.string("keyboard.error.manualOpenForFlow")
|
||||
} else if state.isLocalEngine {
|
||||
msg = "系统拒绝了键盘跳转。请先手动打开 OSGKeyboard 完成本地转写,再返回输入页"
|
||||
msg = ExtL10n.string("keyboard.error.manualOpenDictateLocal")
|
||||
} else {
|
||||
msg = "系统拒绝了键盘跳转。请先手动打开 OSGKeyboard 录音,再返回输入页"
|
||||
msg = ExtL10n.string("keyboard.error.manualOpenDictate")
|
||||
}
|
||||
state.phase = .error(.unknown(msg), message: msg)
|
||||
scheduleAutoClearError()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// ExtL10n.swift
|
||||
// OSGKeyboard · Keyboard Extension
|
||||
//
|
||||
// Loads strings from the extension bundle. Replaces the old KeyboardL10n
|
||||
// hard-coded fallback map — keys live in Localizable.strings.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ExtL10n {
|
||||
static func string(_ key: String) -> String {
|
||||
NSLocalizedString(key, bundle: .main, comment: "")
|
||||
}
|
||||
|
||||
static func format(_ key: String, _ args: CVarArg...) -> String {
|
||||
String(format: string(key), locale: Locale.current, arguments: args)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public struct KeyboardRootView: View {
|
||||
.overlay(Circle().stroke(palette.divider, lineWidth: 0.5))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text(KeyboardL10n.openSettingsA11y))
|
||||
.accessibilityLabel(Text("keyboard.openSettingsA11y"))
|
||||
}
|
||||
.padding(.horizontal, Spacing.md)
|
||||
}
|
||||
@@ -99,6 +99,7 @@ public struct KeyboardRootView: View {
|
||||
TranscriptLine(
|
||||
phase: state.phase,
|
||||
transcript: state.lastTranscript,
|
||||
flowSessionActive: state.flowSessionActive,
|
||||
openSettings: state.openSettings
|
||||
)
|
||||
.frame(height: 22)
|
||||
@@ -128,7 +129,7 @@ public struct KeyboardRootView: View {
|
||||
state.deleteBackward()
|
||||
}
|
||||
Button(action: state.insertSpace) {
|
||||
Text(KeyboardL10n.space)
|
||||
Text("keyboard.space")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.frame(maxWidth: .infinity, minHeight: 42)
|
||||
@@ -139,7 +140,7 @@ public struct KeyboardRootView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text(KeyboardL10n.space))
|
||||
.accessibilityLabel(Text("keyboard.space"))
|
||||
ToolbarIconButton(systemName: "return", label: "newline") {
|
||||
state.insertNewline()
|
||||
}
|
||||
@@ -194,19 +195,26 @@ private struct TranscriptLine: View {
|
||||
|
||||
let phase: KeyboardViewController.State.Phase
|
||||
let transcript: String
|
||||
let flowSessionActive: Bool
|
||||
let openSettings: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch phase {
|
||||
case .idle:
|
||||
Text(KeyboardL10n.placeholderIdle)
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
if flowSessionActive {
|
||||
Text("keyboard.placeholder.idle")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
} else {
|
||||
Text("keyboard.flow.sessionInactive")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
}
|
||||
case .requestingPermissions:
|
||||
HStack(spacing: 6) {
|
||||
ProgressView().controlSize(.mini).tint(palette.textSecondary)
|
||||
Text(KeyboardL10n.placeholderPreparing)
|
||||
Text("keyboard.placeholder.preparing")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
@@ -220,7 +228,7 @@ private struct TranscriptLine: View {
|
||||
case .processing:
|
||||
HStack(spacing: 6) {
|
||||
ProgressView().controlSize(.mini).tint(palette.accent)
|
||||
Text(transcript.isEmpty ? KeyboardL10n.placeholderProcessing : transcript)
|
||||
Text(transcript.isEmpty ? String(localized: "keyboard.placeholder.processing") : transcript)
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.lineLimit(1)
|
||||
@@ -247,17 +255,17 @@ private struct TranscriptLine: View {
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityHint(Text(KeyboardL10n.deniedHint))
|
||||
.accessibilityHint(Text("keyboard.deniedHint"))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, Spacing.md)
|
||||
}
|
||||
|
||||
private func deniedMessage(for reason: KeyboardViewController.State.Phase.Reason) -> String {
|
||||
private func deniedMessage(for reason: KeyboardViewController.State.Phase.Reason) -> LocalizedStringKey {
|
||||
switch reason {
|
||||
case .mic: return KeyboardL10n.micDenied
|
||||
case .speech: return KeyboardL10n.speechDenied
|
||||
case .mic: return "keyboard.denied.mic"
|
||||
case .speech: return "keyboard.denied.speech"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,21 +317,21 @@ private struct StatusBadge: View {
|
||||
EmptyView()
|
||||
case .recording:
|
||||
if onDeviceSupported {
|
||||
dot(color: palette.recordRed, label: "REC")
|
||||
dot(color: palette.recordRed, labelKey: "keyboard.status.rec")
|
||||
} else {
|
||||
dot(color: palette.warning, label: "REC ⚠️", showWarning: true)
|
||||
dot(color: palette.warning, labelKey: "keyboard.status.recWarning", showWarning: true)
|
||||
}
|
||||
case .processing:
|
||||
dot(color: palette.accent, label: "···")
|
||||
dot(color: palette.accent, labelKey: "keyboard.status.processing")
|
||||
case .error:
|
||||
dot(color: palette.warning, label: "!")
|
||||
dot(color: palette.warning, labelKey: "keyboard.status.error")
|
||||
case .denied:
|
||||
dot(color: palette.warning, label: "!")
|
||||
dot(color: palette.warning, labelKey: "keyboard.status.error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dot(color: Color, label: String, showWarning: Bool = false) -> some View {
|
||||
private func dot(color: Color, labelKey: LocalizedStringKey, showWarning: Bool = false) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
Circle()
|
||||
.fill(color)
|
||||
@@ -333,7 +341,7 @@ private struct StatusBadge: View {
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
.foregroundStyle(palette.warning)
|
||||
}
|
||||
Text(label)
|
||||
Text(labelKey)
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
@@ -352,7 +360,7 @@ private struct LocalEngineChip: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "iphone.badge.checkmark")
|
||||
Text(KeyboardL10n.localBadge)
|
||||
Text("keyboard.placeholder.localBadge")
|
||||
}
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.accent)
|
||||
@@ -363,34 +371,6 @@ private struct LocalEngineChip: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Extension text fallback
|
||||
//
|
||||
// Custom keyboard extensions can end up without the expected localized
|
||||
// resource table when signing/project generation drifts. Keep a tiny
|
||||
// in-code fallback map so UI never shows raw key names like
|
||||
// "common.space" in production.
|
||||
private enum KeyboardL10n {
|
||||
private static var isChinese: Bool {
|
||||
Locale.preferredLanguages.first?.hasPrefix("zh") == true
|
||||
}
|
||||
|
||||
static var space: String { isChinese ? "空格" : "Space" }
|
||||
static var placeholderIdle: String { isChinese ? "点按说话" : "Tap to talk" }
|
||||
static var placeholderPreparing: String { isChinese ? "准备中…" : "Preparing" }
|
||||
static var placeholderProcessing: String { isChinese ? "处理中…" : "Processing" }
|
||||
static var localBadge: String { isChinese ? "本地" : "On-device" }
|
||||
static var micDenied: String { isChinese ? "麦克风被拒绝" : "Mic denied" }
|
||||
static var speechDenied: String { isChinese ? "语音识别被拒绝" : "Speech denied" }
|
||||
static var deniedHint: String {
|
||||
isChinese
|
||||
? "打开 OSGKeyboard 设置,可授予麦克风 / 语音识别权限。"
|
||||
: "Open OSGKeyboard settings to grant microphone / speech access."
|
||||
}
|
||||
static var openSettingsA11y: String {
|
||||
isChinese ? "打开 OSGKeyboard 设置" : "Open OSGKeyboard settings"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mode chip
|
||||
|
||||
private struct ModeChip: View {
|
||||
@@ -429,12 +409,8 @@ private struct ModeChip: View {
|
||||
.menuStyle(.button)
|
||||
}
|
||||
|
||||
private func label(for m: KeyboardViewController.State.InputMode) -> String {
|
||||
switch m {
|
||||
case .off: return "Off"
|
||||
case .transcribe: return "转写"
|
||||
case .polish: return "润色"
|
||||
}
|
||||
private func label(for m: KeyboardViewController.State.InputMode) -> LocalizedStringKey {
|
||||
LocalizedStringKey(m.labelKey)
|
||||
}
|
||||
|
||||
private func icon(for m: KeyboardViewController.State.InputMode) -> String {
|
||||
@@ -454,13 +430,13 @@ private struct LocaleChip: View {
|
||||
let localeId: String
|
||||
let onChange: (String) -> Void
|
||||
|
||||
private let options: [(id: String, label: String)] = [
|
||||
("auto", "Auto"),
|
||||
("zh-Hans", "简体"),
|
||||
("zh-Hant", "繁體"),
|
||||
("en-US", "EN"),
|
||||
("ja-JP", "日"),
|
||||
("ko-KR", "한")
|
||||
private let options: [(id: String, labelKey: String)] = [
|
||||
("auto", "locale.chip.auto"),
|
||||
("zh-Hans", "locale.chip.zh-Hans"),
|
||||
("zh-Hant", "locale.chip.zh-Hant"),
|
||||
("en-US", "locale.chip.en-US"),
|
||||
("ja-JP", "locale.chip.ja-JP"),
|
||||
("ko-KR", "locale.chip.ko-KR")
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
@@ -470,9 +446,9 @@ private struct LocaleChip: View {
|
||||
onChange(o.id)
|
||||
} label: {
|
||||
if o.id == localeId {
|
||||
Label(o.label, systemImage: "checkmark")
|
||||
Label(LocalizedStringKey(o.labelKey), systemImage: "checkmark")
|
||||
} else {
|
||||
Text(o.label)
|
||||
Text(LocalizedStringKey(o.labelKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,7 +469,10 @@ private struct LocaleChip: View {
|
||||
.menuStyle(.button)
|
||||
}
|
||||
|
||||
private var currentLabel: String {
|
||||
options.first(where: { $0.id == localeId })?.label ?? "Auto"
|
||||
private var currentLabel: LocalizedStringKey {
|
||||
if let key = options.first(where: { $0.id == localeId })?.labelKey {
|
||||
return LocalizedStringKey(key)
|
||||
}
|
||||
return "locale.chip.auto"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,51 @@
|
||||
"keyboard.deniedHint" = "Opens the OSGKeyboard settings page where you can grant microphone or speech recognition access.";
|
||||
"keyboard.tapToTalkA11y" = "Tap to talk";
|
||||
|
||||
/* Flow session (keyboard) */
|
||||
"keyboard.flow.sessionInactive" = "Open OSGKeyboard to start voice session";
|
||||
"keyboard.flow.sessionExpired" = "Voice session ended. Open OSGKeyboard to restart.";
|
||||
"keyboard.flow.startingSession" = "Starting voice session…";
|
||||
"keyboard.flow.transcribing" = "Transcribing…";
|
||||
"keyboard.flow.resultTimeout" = "Timed out waiting for transcription. Try again.";
|
||||
"keyboard.flow.manualOpenHost" = "Could not auto-open the app. Open OSGKeyboard from the Home Screen, then return.";
|
||||
|
||||
/* Keyboard status badges */
|
||||
"keyboard.status.rec" = "REC";
|
||||
"keyboard.status.recWarning" = "REC";
|
||||
"keyboard.status.processing" = "···";
|
||||
"keyboard.status.error" = "!";
|
||||
|
||||
/* Keyboard errors */
|
||||
"keyboard.error.appGroupUnavailable" = "App Group not configured";
|
||||
"keyboard.error.appGroupCommunication" = "App Group not configured. Reinstall and check signing.";
|
||||
"keyboard.error.fullAccessRequired" = "Enable Allow Full Access for OSGKeyboard in Settings to use voice input.";
|
||||
"keyboard.error.fullAccessForJump" = "Allow Full Access is required before the keyboard can open the app.";
|
||||
"keyboard.error.manualOpenSettings" = "System blocked the jump. Open OSGKeyboard manually to reach Settings.";
|
||||
"keyboard.error.manualOpenForFlow" = "Voice session not running. Open OSGKeyboard, then tap the mic again.";
|
||||
"keyboard.error.manualOpenDictateLocal" = "System blocked the jump. Open OSGKeyboard for on-device dictation, then return.";
|
||||
"keyboard.error.manualOpenDictate" = "System blocked the jump. Open OSGKeyboard to record, then return.";
|
||||
"keyboard.error.llm.noApiKey" = "API key missing · configure it in the main app";
|
||||
"keyboard.error.llm.unauthorized" = "Invalid API key (401) · check main app settings";
|
||||
"keyboard.error.llm.rateLimited" = "Rate limited (429) · try again later";
|
||||
|
||||
/* Legacy dictation handoff progress */
|
||||
"keyboard.dictation.opening" = "Opening OSGKeyboard…";
|
||||
"keyboard.dictation.openingLocal" = "Opening OSGKeyboard (on-device)…";
|
||||
"keyboard.dictation.recording" = "Recording in OSGKeyboard — return here when done";
|
||||
"keyboard.dictation.recordingLocal" = "Recording on-device — return here when done";
|
||||
"keyboard.dictation.transcribing" = "Transcribing — return here when ready";
|
||||
"keyboard.dictation.transcribingLocal" = "On-device transcribing — return here when ready";
|
||||
"keyboard.dictation.failed" = "Recording failed. Try again.";
|
||||
"keyboard.dictation.resultTimeout" = "Timed out waiting for dictation. Finish in OSGKeyboard and retry.";
|
||||
|
||||
/* Locale chip (short labels) */
|
||||
"locale.chip.auto" = "Auto";
|
||||
"locale.chip.zh-Hans" = "简";
|
||||
"locale.chip.zh-Hant" = "繁";
|
||||
"locale.chip.en-US" = "EN";
|
||||
"locale.chip.ja-JP" = "日";
|
||||
"locale.chip.ko-KR" = "韩";
|
||||
|
||||
/* Mode chip labels (used in both ext + preview stub) */
|
||||
"mode.off" = "Off";
|
||||
"mode.transcribe" = "Transcribe";
|
||||
|
||||
@@ -126,6 +126,51 @@
|
||||
"keyboard.deniedHint" = "打开 OSGKeyboard 设置,可授予麦克风 / 语音识别权限。";
|
||||
"keyboard.tapToTalkA11y" = "点按说话";
|
||||
|
||||
/* Flow session (keyboard) */
|
||||
"keyboard.flow.sessionInactive" = "请打开 OSGKeyboard 启动语音会话";
|
||||
"keyboard.flow.sessionExpired" = "语音会话已结束,请打开 OSGKeyboard 重新启动";
|
||||
"keyboard.flow.startingSession" = "正在启动语音会话…";
|
||||
"keyboard.flow.transcribing" = "识别中…";
|
||||
"keyboard.flow.resultTimeout" = "等待识别结果超时,请重试";
|
||||
"keyboard.flow.manualOpenHost" = "无法自动跳转,请从主屏幕打开 OSGKeyboard 后返回";
|
||||
|
||||
/* Keyboard status badges */
|
||||
"keyboard.status.rec" = "录音";
|
||||
"keyboard.status.recWarning" = "录音";
|
||||
"keyboard.status.processing" = "···";
|
||||
"keyboard.status.error" = "!";
|
||||
|
||||
/* Keyboard errors */
|
||||
"keyboard.error.appGroupUnavailable" = "App Group 未配置";
|
||||
"keyboard.error.appGroupCommunication" = "App Group 未配置,请重新安装并检查签名";
|
||||
"keyboard.error.fullAccessRequired" = "请在系统设置中为 OSGKeyboard 开启「允许完全访问」";
|
||||
"keyboard.error.fullAccessForJump" = "请先开启「允许完全访问」,否则键盘无法跳转到 App";
|
||||
"keyboard.error.manualOpenSettings" = "系统拒绝了跳转,请手动打开 OSGKeyboard 进入设置";
|
||||
"keyboard.error.manualOpenForFlow" = "语音会话未启动,请打开 OSGKeyboard 后再点麦克风";
|
||||
"keyboard.error.manualOpenDictateLocal" = "系统拒绝了跳转,请手动打开 OSGKeyboard 完成本地转写";
|
||||
"keyboard.error.manualOpenDictate" = "系统拒绝了跳转,请手动打开 OSGKeyboard 录音";
|
||||
"keyboard.error.llm.noApiKey" = "未配置 API Key · 请在主 App 设置中填写";
|
||||
"keyboard.error.llm.unauthorized" = "API Key 无效 (401) · 请检查主 App 设置";
|
||||
"keyboard.error.llm.rateLimited" = "API 限流 (429) · 请稍后再试";
|
||||
|
||||
/* Legacy dictation handoff progress */
|
||||
"keyboard.dictation.opening" = "正在打开 OSGKeyboard…";
|
||||
"keyboard.dictation.openingLocal" = "正在打开 OSGKeyboard(本地转写)…";
|
||||
"keyboard.dictation.recording" = "正在录音,请完成后返回当前输入页";
|
||||
"keyboard.dictation.recordingLocal" = "正在本地录音,请完成后返回当前输入页";
|
||||
"keyboard.dictation.transcribing" = "识别中,请稍候并返回输入页";
|
||||
"keyboard.dictation.transcribingLocal" = "本地识别中,请稍候并返回输入页";
|
||||
"keyboard.dictation.failed" = "录音失败,请重试";
|
||||
"keyboard.dictation.resultTimeout" = "等待录音结果超时,请返回 OSGKeyboard 完成录音后重试";
|
||||
|
||||
/* Locale chip (short labels) */
|
||||
"locale.chip.auto" = "自动";
|
||||
"locale.chip.zh-Hans" = "简";
|
||||
"locale.chip.zh-Hant" = "繁";
|
||||
"locale.chip.en-US" = "EN";
|
||||
"locale.chip.ja-JP" = "日";
|
||||
"locale.chip.ko-KR" = "韩";
|
||||
|
||||
/* Mode chip labels */
|
||||
"mode.off" = "关闭";
|
||||
"mode.transcribe" = "转写";
|
||||
|
||||
Reference in New Issue
Block a user