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:
@@ -57,25 +57,21 @@ struct LocalModelsGroup: View {
|
|||||||
/// itself is always live (the user can flip it without having a
|
/// itself is always live (the user can flip it without having a
|
||||||
/// key yet), but the polish call short-circuits with an Alert if
|
/// key yet), but the polish call short-circuits with an Alert if
|
||||||
/// the Keychain is empty when it fires.
|
/// the Keychain is empty when it fires.
|
||||||
|
///
|
||||||
|
/// v0.2.1: dropped the long descriptive subtitle — it explained
|
||||||
|
/// things the user could read about elsewhere in Settings
|
||||||
|
/// (provider / API key section) and made the row visually heavy.
|
||||||
|
/// Title + switch is enough; details live in `CloudPolishDisclosureBanner`.
|
||||||
private var cloudPolishRow: some View {
|
private var cloudPolishRow: some View {
|
||||||
VStack(alignment: .leading, spacing: Spacing.xs) {
|
Toggle(isOn: $config.localModeCloudPolishEnabled) {
|
||||||
Toggle(isOn: $config.localModeCloudPolishEnabled) {
|
Text("settings.localModels.cloudPolish.title")
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
.font(TypeStyle.body)
|
||||||
Text("settings.localModels.cloudPolish.title")
|
.foregroundStyle(palette.textPrimary)
|
||||||
.font(TypeStyle.body)
|
|
||||||
.foregroundStyle(palette.textPrimary)
|
|
||||||
Text("settings.localModels.cloudPolish.subtitle")
|
|
||||||
.font(TypeStyle.caption2)
|
|
||||||
.foregroundStyle(palette.textTertiary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toggleStyle(.switch)
|
|
||||||
.tint(palette.accent)
|
|
||||||
}
|
}
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
.tint(palette.accent)
|
||||||
.padding(.horizontal, Spacing.md)
|
.padding(.horizontal, Spacing.md)
|
||||||
.padding(.vertical, Spacing.sm)
|
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
||||||
.frame(minHeight: SettingsListMetrics.doubleLineMinHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helpers
|
// MARK: Helpers
|
||||||
|
|||||||
@@ -756,8 +756,36 @@ private struct APISetupPage: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, Spacing.lg)
|
.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)
|
.padding(.bottom, Spacing.xxxl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var translationSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
||||||
|
Text("settings.translation.title")
|
||||||
|
.font(TypeStyle.caption2)
|
||||||
|
.foregroundStyle(palette.textSecondary)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
TranslationPickerRow(config: config)
|
||||||
|
}
|
||||||
|
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
|
||||||
|
.stroke(palette.divider, lineWidth: 0.5)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,17 +62,16 @@ struct SettingsView: View {
|
|||||||
VStack(spacing: Spacing.md) {
|
VStack(spacing: Spacing.md) {
|
||||||
appLanguageSection
|
appLanguageSection
|
||||||
engineSection
|
engineSection
|
||||||
|
// v0.2.1: hide provider/api card when the
|
||||||
|
// local engine is active regardless of the
|
||||||
|
// cloud-polish toggle. Local mode is
|
||||||
|
// contractually ASR-only, so provider/model/
|
||||||
|
// base URL/API key controls have no use —
|
||||||
|
// and exposing them invites the user to fill
|
||||||
|
// out a DeepSeek key they can't use.
|
||||||
if config.engineMode == "cloud" {
|
if config.engineMode == "cloud" {
|
||||||
providerSection
|
providerSection
|
||||||
apiSection
|
apiSection
|
||||||
} else if config.localModeCloudPolishEnabled {
|
|
||||||
// v0.2.0: local engine + cloud polish on.
|
|
||||||
// Surface the provider / API key fields so
|
|
||||||
// the user can fill in their DeepSeek key.
|
|
||||||
// We hide them when the toggle is off so the
|
|
||||||
// local engine stays genuinely local.
|
|
||||||
providerSection
|
|
||||||
apiSection
|
|
||||||
}
|
}
|
||||||
languageAndModelsSection
|
languageAndModelsSection
|
||||||
if config.engineMode == "cloud" {
|
if config.engineMode == "cloud" {
|
||||||
@@ -128,10 +127,12 @@ struct SettingsView: View {
|
|||||||
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
||||||
sectionHeader("settings.language.title")
|
sectionHeader("settings.language.title")
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if config.engineMode == "local" {
|
// v0.2.1: language tab reorder — ASR locale ("识别语言")
|
||||||
LocalModelsGroup(config: config)
|
// now sits above the cloud-polish toggle / local models
|
||||||
Divider().background(palette.divider)
|
// block so the row that maps to microphone input comes
|
||||||
}
|
// first, the row that maps to post-processing comes
|
||||||
|
// second, and translation (post-post-processing) sits at
|
||||||
|
// the bottom.
|
||||||
LocalePickerRow(
|
LocalePickerRow(
|
||||||
locales: effectiveLocales,
|
locales: effectiveLocales,
|
||||||
selection: Binding(
|
selection: Binding(
|
||||||
@@ -139,11 +140,10 @@ struct SettingsView: View {
|
|||||||
set: { config.localeId = $0 }
|
set: { config.localeId = $0 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// v0.2.1: translation toggle + target-language picker.
|
if config.engineMode == "local" {
|
||||||
// Sits at the bottom of the language section so the user
|
Divider().background(palette.divider)
|
||||||
// finds it next to the ASR locale it complements. Reuses
|
LocalModelsGroup(config: config)
|
||||||
// `config.translationEnabled` / `config.translationTargetLocaleId`
|
}
|
||||||
// bindings — no new state, no new persistence path.
|
|
||||||
Divider().background(palette.divider)
|
Divider().background(palette.divider)
|
||||||
TranslationPickerRow(config: config)
|
TranslationPickerRow(config: config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
// TranslationPickerRow.swift
|
// TranslationPickerRow.swift
|
||||||
// OSGKeyboard · Main App
|
// OSGKeyboard · Main App
|
||||||
//
|
//
|
||||||
// List row that hosts the translation toggle + target-language picker.
|
// Single-row "翻译" picker — replaces the previous two-row toggle +
|
||||||
// Lives at the bottom of the language tab in Settings (see
|
// target-locale dropdown. Lets the user pick "不翻译" (off, the
|
||||||
// `SettingsView.languageAndModelsSection`) so it sits right next to
|
// default) or one of the 10 target languages, all from a single
|
||||||
// the existing ASR locale picker — same picker family, same row
|
// `Menu`.
|
||||||
// metrics.
|
|
||||||
//
|
//
|
||||||
// Layout:
|
// Mapping to persisted state:
|
||||||
// • First row → switch (label on the left, switch on the right)
|
// • "不翻译" → translationEnabled = false
|
||||||
// • When on → a second row with a Menu picker for the target
|
// • any specific locale → translationEnabled = true, translationTargetLocaleId = <id>
|
||||||
// language. Disabled when the local engine is active so the user
|
|
||||||
// immediately sees why the picker is greyed out (instead of picking
|
|
||||||
// a target that the pipeline silently ignores).
|
|
||||||
//
|
//
|
||||||
// Reuses the host app's `ProviderConfig` `translationEnabled` /
|
// The local engine constraint ("translation is cloud-only") is handled
|
||||||
// `translationTargetLocaleId` bindings — no new state, no new
|
// in two places that read this row:
|
||||||
// persistence path.
|
// • 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.
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import OSGKeyboardShared
|
import OSGKeyboardShared
|
||||||
@@ -26,90 +27,87 @@ struct TranslationPickerRow: View {
|
|||||||
@ObservedObject var config: ProviderConfig
|
@ObservedObject var config: ProviderConfig
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
|
||||||
toggleRow
|
|
||||||
if config.translationEnabled {
|
|
||||||
Divider().background(palette.divider)
|
|
||||||
targetRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var toggleRow: some View {
|
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
Text("settings.translation.title")
|
||||||
Text("settings.translation.title")
|
|
||||||
.font(TypeStyle.body)
|
|
||||||
.foregroundStyle(palette.textPrimary)
|
|
||||||
Text(subtitleKey)
|
|
||||||
.font(TypeStyle.caption2)
|
|
||||||
.foregroundStyle(palette.textSecondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Toggle(
|
|
||||||
"",
|
|
||||||
isOn: Binding(
|
|
||||||
get: { config.translationEnabled },
|
|
||||||
set: { config.translationEnabled = $0 }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.labelsHidden()
|
|
||||||
.tint(palette.accent)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, Spacing.md)
|
|
||||||
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subtitle explains why the toggle is functionally inert when the
|
|
||||||
/// local engine is on. We still let the user flip the toggle in
|
|
||||||
/// that case so their preference is saved — the moment they switch
|
|
||||||
/// back to cloud, translation just works.
|
|
||||||
private var subtitleKey: LocalizedStringKey {
|
|
||||||
config.isLocalEngine
|
|
||||||
? "settings.translation.subtitle.needsCloud"
|
|
||||||
: "settings.translation.subtitle"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var targetRow: some View {
|
|
||||||
HStack {
|
|
||||||
Text("settings.translation.target")
|
|
||||||
.font(TypeStyle.body)
|
.font(TypeStyle.body)
|
||||||
.foregroundStyle(palette.textPrimary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(TranslationLanguageCatalog.all) { language in
|
ForEach(TranslationLanguageCatalog.all) { language in
|
||||||
Button {
|
Button {
|
||||||
config.translationTargetLocaleId = language.id
|
apply(language)
|
||||||
} label: {
|
} label: {
|
||||||
if language.id == config.translationTargetLocaleId {
|
if currentSelectionId == language.id {
|
||||||
Label(language.nativeName, systemImage: "checkmark")
|
Label(displayLabel(for: language), systemImage: "checkmark")
|
||||||
} else {
|
} else {
|
||||||
Text(language.nativeName)
|
Text(displayLabel(for: language))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(currentTargetName)
|
Text(currentLabel)
|
||||||
.font(TypeStyle.body)
|
.font(TypeStyle.body)
|
||||||
.foregroundStyle(pickerForeground)
|
.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)
|
||||||
|
}
|
||||||
Image(systemName: "chevron.up.chevron.down")
|
Image(systemName: "chevron.up.chevron.down")
|
||||||
.font(.system(size: 11, weight: .bold))
|
.font(.system(size: 11, weight: .bold))
|
||||||
.foregroundStyle(palette.textTertiary)
|
.foregroundStyle(palette.textTertiary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(config.isLocalEngine)
|
|
||||||
.opacity(config.isLocalEngine ? 0.5 : 1.0)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Spacing.md)
|
.padding(.horizontal, Spacing.md)
|
||||||
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentTargetName: String {
|
// MARK: - Selection plumbing
|
||||||
TranslationLanguageCatalog.resolve(config.translationTargetLocaleId).nativeName
|
|
||||||
|
/// 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`).
|
||||||
|
private var currentSelectionId: String {
|
||||||
|
config.translationEnabled
|
||||||
|
? config.translationTargetLocaleId
|
||||||
|
: TranslationLanguageCatalog.offLocaleId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentLabel: String {
|
||||||
|
displayLabel(for: TranslationLanguageCatalog.resolve(currentSelectionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentIsOff: Bool {
|
||||||
|
TranslationLanguageCatalog.isOff(currentSelectionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayLabel(for language: TranslationLanguage) -> String {
|
||||||
|
if language.id == TranslationLanguageCatalog.offLocaleId {
|
||||||
|
return AppL10n.string("settings.translation.off")
|
||||||
|
}
|
||||||
|
return language.nativeName
|
||||||
}
|
}
|
||||||
|
|
||||||
private var pickerForeground: Color {
|
private var pickerForeground: Color {
|
||||||
config.isLocalEngine ? palette.textTertiary : palette.textSecondary
|
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.
|
||||||
|
private func apply(_ language: TranslationLanguage) {
|
||||||
|
if language.id == TranslationLanguageCatalog.offLocaleId {
|
||||||
|
config.translationEnabled = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.translationEnabled = true
|
||||||
|
config.translationTargetLocaleId = language.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,10 +109,9 @@
|
|||||||
"settings.api.title" = "API";
|
"settings.api.title" = "API";
|
||||||
"settings.language.title" = "Language";
|
"settings.language.title" = "Language";
|
||||||
// v0.2.1: translation feature
|
// v0.2.1: translation feature
|
||||||
"settings.translation.title" = "Translate after dictation";
|
"settings.translation.title" = "Translation";
|
||||||
"settings.translation.subtitle" = "Send the transcript to your LLM with a translate-and-polish prompt before inserting.";
|
"settings.translation.off" = "Don't translate";
|
||||||
"settings.translation.subtitle.needsCloud" = "Requires the cloud engine — currently disabled.";
|
"settings.translation.hint.needsCloud" = "Needs cloud";
|
||||||
"settings.translation.target" = "Target language";
|
|
||||||
"settings.languageModels.title" = "Language & models";
|
"settings.languageModels.title" = "Language & models";
|
||||||
"settings.localModels.title" = "On-device models";
|
"settings.localModels.title" = "On-device models";
|
||||||
"settings.localModels.speechRole" = "Speech";
|
"settings.localModels.speechRole" = "Speech";
|
||||||
|
|||||||
@@ -109,10 +109,9 @@
|
|||||||
"settings.api.title" = "接口";
|
"settings.api.title" = "接口";
|
||||||
"settings.language.title" = "语言";
|
"settings.language.title" = "语言";
|
||||||
// v0.2.1: 翻译功能
|
// v0.2.1: 翻译功能
|
||||||
"settings.translation.title" = "语音转文字后翻译";
|
"settings.translation.title" = "翻译";
|
||||||
"settings.translation.subtitle" = "将转写文本发送给已配置的 LLM,使用翻译+润色 prompt 处理后插入。";
|
"settings.translation.off" = "不翻译";
|
||||||
"settings.translation.subtitle.needsCloud" = "需开启云端引擎,当前为本地模式暂不生效。";
|
"settings.translation.hint.needsCloud" = "需云端";
|
||||||
"settings.translation.target" = "目标语言";
|
|
||||||
"settings.languageModels.title" = "语言与模型";
|
"settings.languageModels.title" = "语言与模型";
|
||||||
"settings.localModels.title" = "本地模型";
|
"settings.localModels.title" = "本地模型";
|
||||||
"settings.localModels.speechRole" = "语音识别";
|
"settings.localModels.speechRole" = "语音识别";
|
||||||
|
|||||||
@@ -28,12 +28,22 @@ public struct TranslationLanguage: Identifiable, Hashable, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum TranslationLanguageCatalog {
|
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"
|
public static let defaultLocaleId = "en"
|
||||||
|
|
||||||
/// Curated set. Order matters — the picker / chip render top-to-
|
/// 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] = [
|
public static let all: [TranslationLanguage] = [
|
||||||
|
TranslationLanguage(id: offLocaleId, promptLanguageName: "", nativeName: ""),
|
||||||
TranslationLanguage(id: "en", promptLanguageName: "English", nativeName: "English"),
|
TranslationLanguage(id: "en", promptLanguageName: "English", nativeName: "English"),
|
||||||
TranslationLanguage(id: "zh-Hans", promptLanguageName: "Simplified Chinese", nativeName: "简体中文"),
|
TranslationLanguage(id: "zh-Hans", promptLanguageName: "Simplified Chinese", nativeName: "简体中文"),
|
||||||
TranslationLanguage(id: "zh-Hant", promptLanguageName: "Traditional 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"),
|
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
|
/// Resolve a stored locale id to its catalog entry. Falls back to
|
||||||
/// `defaultLocaleId` when the id is missing or unknown — matches the
|
/// `offLocaleId` (the picker default) when the id is missing or
|
||||||
/// pattern used elsewhere (e.g. `ASRLocaleLabels`) so the keyboard
|
/// unknown — matches the pattern used elsewhere (e.g.
|
||||||
/// never crashes on a stale persisted value.
|
/// `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 {
|
public static func resolve(_ id: String) -> TranslationLanguage {
|
||||||
if let match = all.first(where: { $0.id == id }) {
|
if let match = all.first(where: { $0.id == id }) {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
return all.first { $0.id == defaultLocaleId } ?? all[0]
|
return all.first { $0.id == offLocaleId } ?? all[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user