feat(translation-v2): refactor translationEnabled into computed + onboarding row visibility
This commit is contained in:
@@ -734,6 +734,18 @@ private struct APISetupPage: View {
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
APISettingsCard(config: config)
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
// v0.2.1 follow-up: translation row lives on the
|
||||
// onboarding engine page so first-time users can
|
||||
// pick a target language before they ever see the
|
||||
// keyboard. Wrapped in the section's surface card
|
||||
// so it sits flush with the engine / provider cards
|
||||
// above; visibility gated by `isTranslationRowVisible`
|
||||
// so the local engine only shows it when cloud
|
||||
// polish is also enabled.
|
||||
if config.isTranslationRowVisible {
|
||||
translationSection
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
}
|
||||
} else {
|
||||
// v0.2.0: local engine is iOS `SpeechAnalyzer` only.
|
||||
// Surface the cloud-polish toggle and a one-line
|
||||
@@ -755,22 +767,25 @@ private struct APISetupPage: View {
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
// v0.2.1 follow-up: same conditional for the local
|
||||
// branch — the row only renders when the engine
|
||||
// can actually run the cloud translate-and-polish
|
||||
// step (i.e. cloud polish is opted in).
|
||||
if config.isTranslationRowVisible {
|
||||
translationSection
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
}
|
||||
}
|
||||
|
||||
// v0.2.1: translation row lives on the onboarding engine
|
||||
// page so first-time users can pick a target language
|
||||
// before they ever see the keyboard. We render the same
|
||||
// `TranslationPickerRow` used in the language tab — same
|
||||
// persisted bindings, same "需云端" hint when local is
|
||||
// active — wrapped in the section's surface card so it
|
||||
// sits flush with the engine / provider cards above.
|
||||
translationSection
|
||||
.padding(.horizontal, Spacing.lg)
|
||||
}
|
||||
.padding(.bottom, Spacing.xxxl)
|
||||
}
|
||||
}
|
||||
|
||||
/// v0.2.1 follow-up: extracted so both engine branches can render
|
||||
/// the same surface card + picker. `TranslationPickerRow` itself
|
||||
/// reads `ProviderConfig.translationTargetLocaleId` directly, so
|
||||
/// picking a locale in onboarding flows through to the keyboard
|
||||
/// extension on the next `load()` cycle.
|
||||
private var translationSection: some View {
|
||||
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
||||
Text("settings.translation.title")
|
||||
@@ -779,7 +794,7 @@ private struct APISetupPage: View {
|
||||
.textCase(.uppercase)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
VStack(spacing: 0) {
|
||||
TranslationPickerRow(config: config)
|
||||
TranslationPickerRow(config: config, isVisible: true)
|
||||
}
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
||||
.overlay(
|
||||
|
||||
@@ -145,7 +145,9 @@ struct SettingsView: View {
|
||||
LocalModelsGroup(config: config)
|
||||
}
|
||||
Divider().background(palette.divider)
|
||||
TranslationPickerRow(config: config)
|
||||
if config.isTranslationRowVisible {
|
||||
TranslationPickerRow(config: config, isVisible: true)
|
||||
}
|
||||
}
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
||||
.overlay(
|
||||
|
||||
@@ -6,18 +6,20 @@
|
||||
// default) or one of the 10 target languages, all from a single
|
||||
// `Menu`.
|
||||
//
|
||||
// Mapping to persisted state:
|
||||
// • "不翻译" → translationEnabled = false
|
||||
// • any specific locale → translationEnabled = true, translationTargetLocaleId = <id>
|
||||
// v0.2.1 follow-up: row is rendered through an `isVisible` parameter
|
||||
// so callers (`SettingsView`, `OnboardingView`) can drop the row
|
||||
// entirely when the engine can't run the cloud translate-and-polish
|
||||
// step (`ProviderConfig.isTranslationRowVisible`). The "needs cloud"
|
||||
// inline hint was deleted along with the previous Bool toggle — the
|
||||
// user only sees the row when the engine can act on the choice.
|
||||
//
|
||||
// The local engine constraint ("translation is cloud-only") is handled
|
||||
// in two places that read this row:
|
||||
// • The picker greys out specific locales (and prepends a "需云端"
|
||||
// hint) when `config.isLocalEngine` — the user can still pick
|
||||
// something but it won't fire end-to-end until they switch engines.
|
||||
// • The pipeline (`PolishingService`) rejects `.translate` on local
|
||||
// engine with `translationNotAvailable`, which `KeyboardViewController`
|
||||
// surfaces as a 2.4s toast. Belt + suspenders.
|
||||
// Mapping to persisted state:
|
||||
// • "不翻译" → translationTargetLocaleId = "off"
|
||||
// • any specific locale → translationTargetLocaleId = <id>
|
||||
//
|
||||
// The pipeline (`PolishingService`) still rejects `.translate` on the
|
||||
// local engine when cloud polish is off, which
|
||||
// `KeyboardViewController` surfaces as a 2.4s toast. Belt + suspenders.
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
@@ -26,57 +28,54 @@ struct TranslationPickerRow: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
@ObservedObject var config: ProviderConfig
|
||||
|
||||
/// Visibility flag — when `false` the row renders as `EmptyView`
|
||||
/// (callers can also wrap the call site in `if` for symmetry, but
|
||||
/// having the guard here means a forgotten `if` still produces a
|
||||
/// safe no-op rather than a leaked dead row).
|
||||
var isVisible: Bool = true
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("settings.translation.title")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Spacer()
|
||||
Menu {
|
||||
ForEach(TranslationLanguageCatalog.all) { language in
|
||||
Button {
|
||||
apply(language)
|
||||
} label: {
|
||||
if currentSelectionId == language.id {
|
||||
Label(displayLabel(for: language), systemImage: "checkmark")
|
||||
} else {
|
||||
Text(displayLabel(for: language))
|
||||
if isVisible {
|
||||
HStack {
|
||||
Text("settings.translation.title")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Spacer()
|
||||
Menu {
|
||||
ForEach(TranslationLanguageCatalog.all) { language in
|
||||
Button {
|
||||
apply(language)
|
||||
} label: {
|
||||
if currentSelectionId == language.id {
|
||||
Label(displayLabel(for: language), systemImage: "checkmark")
|
||||
} else {
|
||||
Text(displayLabel(for: language))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(currentLabel)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(pickerForeground)
|
||||
if config.isLocalEngine && !currentIsOff {
|
||||
// Inline hint so the user knows the picker
|
||||
// selection is being held but won't fire on the
|
||||
// local engine.
|
||||
Text("settings.translation.hint.needsCloud")
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.warning)
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(currentLabel)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(currentIsOff ? palette.textSecondary : palette.textPrimary)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
}
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Spacing.md)
|
||||
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
||||
}
|
||||
.padding(.horizontal, Spacing.md)
|
||||
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
||||
}
|
||||
|
||||
// MARK: - Selection plumbing
|
||||
|
||||
/// Currently selected id derived from `translationEnabled`. We
|
||||
/// route everything through the `translationEnabled` boolean so the
|
||||
/// picker stays in lock-step with the rest of the system (chip,
|
||||
/// pipeline, `isTranslationEffective`).
|
||||
/// Currently selected id — the picker always reads
|
||||
/// `translationTargetLocaleId` directly (the previous
|
||||
/// `translationEnabled` boolean is now derived from it).
|
||||
private var currentSelectionId: String {
|
||||
config.translationEnabled
|
||||
? config.translationTargetLocaleId
|
||||
: TranslationLanguageCatalog.offLocaleId
|
||||
config.translationTargetLocaleId
|
||||
}
|
||||
|
||||
private var currentLabel: String {
|
||||
@@ -94,20 +93,11 @@ struct TranslationPickerRow: View {
|
||||
return language.nativeName
|
||||
}
|
||||
|
||||
private var pickerForeground: Color {
|
||||
currentIsOff ? palette.textSecondary : palette.textPrimary
|
||||
}
|
||||
|
||||
/// Translates a picker choice into the underlying
|
||||
/// `translationEnabled` + `translationTargetLocaleId` pair. Picking
|
||||
/// "不翻译" clears the toggle; any locale flips it on and stores
|
||||
/// the id.
|
||||
/// Translates a picker choice into a single persisted field.
|
||||
/// "不翻译" writes `offLocaleId`; any concrete locale writes its
|
||||
/// id. `ProviderConfig.translationEnabled` is derived from the
|
||||
/// resulting value, so callers don't need to flip a separate Bool.
|
||||
private func apply(_ language: TranslationLanguage) {
|
||||
if language.id == TranslationLanguageCatalog.offLocaleId {
|
||||
config.translationEnabled = false
|
||||
return
|
||||
}
|
||||
config.translationEnabled = true
|
||||
config.translationTargetLocaleId = language.id
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,6 @@
|
||||
// v0.2.1: translation feature
|
||||
"settings.translation.title" = "Translation";
|
||||
"settings.translation.off" = "Don't translate";
|
||||
"settings.translation.hint.needsCloud" = "Needs cloud";
|
||||
"settings.languageModels.title" = "Language & models";
|
||||
"settings.localModels.title" = "On-device models";
|
||||
"settings.localModels.speechRole" = "Speech";
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
// v0.2.1: 翻译功能
|
||||
"settings.translation.title" = "翻译";
|
||||
"settings.translation.off" = "不翻译";
|
||||
"settings.translation.hint.needsCloud" = "需云端";
|
||||
"settings.languageModels.title" = "语言与模型";
|
||||
"settings.localModels.title" = "本地模型";
|
||||
"settings.localModels.speechRole" = "语音识别";
|
||||
|
||||
Reference in New Issue
Block a user