feat(translation-ui): tighten settings/onboarding UX around the translation feature

UI refinements on top of the translation pipeline (feature/translation@HEAD):

1. Onboarding engine page now hosts a translation row.
   APISetupPage renders the same TranslationPickerRow used in the
   language tab, so first-time users can pick a target language
   before they ever see the keyboard. Same persisted bindings; same
   'needs cloud' hint when the local engine is active.

2. Local engine hides the provider / API card unconditionally.
   Removed the 'local + cloud polish on → show API fields' branch
   from SettingsView. Provider/base URL/API key/model controls have
   no use in local mode (translation is cloud-only anyway), and
   exposing them invited users to fill in a DeepSeek key they
   can't use.

3. 'Cloud polish after ASR' toggle loses its long subtitle.
   The descriptive copy in LocalEngineSettingsRows.cloudPolishRow
   was a wall of text that explained things visible elsewhere in
   Settings. Title + switch is enough; the CloudPolishDisclosureBanner
   (rendered by EnginePickerSection when cloud is active) already
   covers the 'this sends text to your API' disclosure.

4. Translation row becomes a single dropdown with a 'Don't
   translate' default.
   TranslationPickerRow replaced with a one-row Menu picker:
   '不翻译 / English / 中文 (简体) / 中文 (繁體) / 日本語 / 한국어 /
   Français / Deutsch / Español / Русский / Português'.
   '不翻译' maps to translationEnabled=false; any locale maps to
   translationEnabled=true + translationTargetLocaleId=<id>.
   TranslationLanguageCatalog gains an 'off' sentinel so the picker's
   single binding stays a plain String.

5. Language tab reorder.
   SettingsView.languageAndModelsSection: ASR locale ('识别语言')
   now sits above the local-models block; translation row sits at
   the bottom. The reading order follows the pipeline direction
   (input → post-processing → post-post-processing).

Localization:
  - 'settings.translation.title' → '翻译' / 'Translation'
  - new 'settings.translation.off' / 'settings.translation.hint.needsCloud'
  - dropped unused subtitle / target-language keys

xcodebuild scheme=OSGKeyboard config=Debug destination=iPhone 17
Simulator: BUILD SUCCEEDED (0 warning, 0 error).
This commit is contained in:
zhongshu
2026-06-25 13:23:54 +08:00
parent deddb49d56
commit 4d92999347
7 changed files with 156 additions and 117 deletions
@@ -28,12 +28,22 @@ public struct TranslationLanguage: Identifiable, Hashable, Sendable {
}
public enum TranslationLanguageCatalog {
/// Default target language id used on fresh installs.
/// Sentinel id for "don't translate" the default selection in the
/// picker. Picked over an `Optional<TranslationLanguage>` so the
/// single-row `Picker` binding stays a plain `String` (and the same
/// code path also works for the `TranslationChip` Menu).
public static let offLocaleId = "off"
/// Default target language id used on fresh installs when translation
/// is enabled. The picker still defaults to `offLocaleId` this is
/// only the language we'd fall back to if a stale "on" state is
/// recovered without a remembered target.
public static let defaultLocaleId = "en"
/// Curated set. Order matters the picker / chip render top-to-
/// bottom, and `defaultLocaleId` is the default selection.
/// bottom, with `offLocaleId` ("") at the very top so the
/// "turn off" action is one tap away from any enabled state.
public static let all: [TranslationLanguage] = [
TranslationLanguage(id: offLocaleId, promptLanguageName: "", nativeName: ""),
TranslationLanguage(id: "en", promptLanguageName: "English", nativeName: "English"),
TranslationLanguage(id: "zh-Hans", promptLanguageName: "Simplified Chinese", nativeName: "简体中文"),
TranslationLanguage(id: "zh-Hant", promptLanguageName: "Traditional Chinese", nativeName: "繁體中文"),
@@ -46,14 +56,23 @@ public enum TranslationLanguageCatalog {
TranslationLanguage(id: "pt", promptLanguageName: "Portuguese", nativeName: "Português"),
]
/// True when the given id is the "off" sentinel. Used by the picker
/// to flip `translationEnabled` and by the pipeline to skip the
/// translate prompt.
public static func isOff(_ id: String) -> Bool {
id == offLocaleId
}
/// Resolve a stored locale id to its catalog entry. Falls back to
/// `defaultLocaleId` when the id is missing or unknown matches the
/// pattern used elsewhere (e.g. `ASRLocaleLabels`) so the keyboard
/// never crashes on a stale persisted value.
/// `offLocaleId` (the picker default) when the id is missing or
/// unknown matches the pattern used elsewhere (e.g.
/// `ASRLocaleLabels`) so the keyboard never crashes on a stale
/// persisted value, and the picker lands on the safe "off" state
/// instead of an arbitrary language.
public static func resolve(_ id: String) -> TranslationLanguage {
if let match = all.first(where: { $0.id == id }) {
return match
}
return all.first { $0.id == defaultLocaleId } ?? all[0]
return all.first { $0.id == offLocaleId } ?? all[0]
}
}