Files
OSGKeyboard/OSGKeyboardShared/Models/TranslationLanguage.swift
T
zhongshu 4d92999347 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).
2026-06-25 13:23:54 +08:00

78 lines
3.9 KiB
Swift

// TranslationLanguage.swift
// OSGKeyboard · Shared
//
// Catalog of target languages the translation feature can produce.
//
// Kept deliberately small (~10 entries) to match the kind of choices
// the user makes in the Settings picker / keyboard chip. We don't try
// to expose every BCP-47 locale — the prompt just needs a target
// language name, and a curated list reads better than a 100-row scroll.
//
// `id` is what gets persisted to the App Group. `promptLanguageName`
// is the human-readable target name injected into the prompt (e.g.
// the LLM sees "English", not "en"). `nativeName` is the endonym we
// show in the picker UI ("日本語" instead of "Japanese").
import Foundation
public struct TranslationLanguage: Identifiable, Hashable, Sendable {
public let id: String
public let promptLanguageName: String
public let nativeName: String
public init(id: String, promptLanguageName: String, nativeName: String) {
self.id = id
self.promptLanguageName = promptLanguageName
self.nativeName = nativeName
}
}
public enum TranslationLanguageCatalog {
/// 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, 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: "繁體中文"),
TranslationLanguage(id: "ja", promptLanguageName: "Japanese", nativeName: "日本語"),
TranslationLanguage(id: "ko", promptLanguageName: "Korean", nativeName: "한국어"),
TranslationLanguage(id: "fr", promptLanguageName: "French", nativeName: "Français"),
TranslationLanguage(id: "de", promptLanguageName: "German", nativeName: "Deutsch"),
TranslationLanguage(id: "es", promptLanguageName: "Spanish", nativeName: "Español"),
TranslationLanguage(id: "ru", promptLanguageName: "Russian", nativeName: "Русский"),
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
/// `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 == offLocaleId } ?? all[0]
}
}