feat(translation-v2): final review — dual-engine translation row + DeepSeek polish override + dead code cleanup

This commit is contained in:
2026-06-25 14:25:06 +08:00
parent 9b759281cc
commit 0956bb8534
12 changed files with 163 additions and 102 deletions
+19 -11
View File
@@ -58,20 +58,28 @@ struct LocalModelsGroup: View {
/// key yet), but the polish call short-circuits with an Alert if
/// 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`.
/// v0.2.1 follow-up: added a one-line caption under the toggle
/// that names the default cloud vendor (DeepSeek) so the user
/// knows where the transcript is going when they flip the switch.
/// The toggle row is now a two-line layout title + caption
/// so we drop the explicit `singleLineMinHeight` here and let
/// `SettingsListMetrics` provide enough vertical room.
private var cloudPolishRow: some View {
Toggle(isOn: $config.localModeCloudPolishEnabled) {
Text("settings.localModels.cloudPolish.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
VStack(alignment: .leading, spacing: 4) {
Toggle(isOn: $config.localModeCloudPolishEnabled) {
Text("settings.localModels.cloudPolish.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
}
.toggleStyle(.switch)
.tint(palette.accent)
Text("settings.localModels.cloudPolish.caption")
.font(TypeStyle.caption2)
.foregroundStyle(palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
.toggleStyle(.switch)
.tint(palette.accent)
.padding(.horizontal, Spacing.md)
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
.padding(.vertical, Spacing.xs)
}
// MARK: Helpers
+14 -10
View File
@@ -737,11 +737,11 @@ private struct APISetupPage: View {
// 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.
// keyboard. v0.2.1 final review: both engines now
// show the row (the local engine routes the polish
// step through DeepSeek, so the constraint is gone).
// Wrapped in the same surface card chrome as the
// APISettingsCard above for visual symmetry.
if config.isTranslationRowVisible {
translationSection
.padding(.horizontal, Spacing.lg)
@@ -767,10 +767,9 @@ 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).
// v0.2.1 final review: same surface card chrome as
// the cloud branch the row now renders for both
// engines.
if config.isTranslationRowVisible {
translationSection
.padding(.horizontal, Spacing.lg)
@@ -786,9 +785,14 @@ private struct APISetupPage: View {
/// reads `ProviderConfig.translationTargetLocaleId` directly, so
/// picking a locale in onboarding flows through to the keyboard
/// extension on the next `load()` cycle.
///
/// v0.2.1 final review: the section header now reads
/// `settings.translation.afterPolish` (renamed alongside the row
/// title in `TranslationPickerRow`) so the section and row read
/// as one cohesive group.
private var translationSection: some View {
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
Text("settings.translation.title")
Text("settings.translation.afterPolish")
.font(TypeStyle.caption2)
.foregroundStyle(palette.textSecondary)
.textCase(.uppercase)
+9 -4
View File
@@ -13,13 +13,18 @@
// inline hint was deleted along with the previous Bool toggle the
// user only sees the row when the engine can act on the choice.
//
// v0.2.1 final review: both engines now run the translate-and-polish
// step (the local engine routes through DeepSeek via
// `ProviderConfig.localModeProviderId`), so the row title changed
// from "Translation" to "Polish then translate" to match the new
// always-on translation contract.
//
// 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.
// The pipeline (`PolishingService`) honors `.translate` on both
// engines when this row is visible no more "rejected mode" toast.
import SwiftUI
import OSGKeyboardShared
@@ -37,7 +42,7 @@ struct TranslationPickerRow: View {
var body: some View {
if isVisible {
HStack {
Text("settings.translation.title")
Text("settings.translation.afterPolish")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
Spacer()
+2 -1
View File
@@ -109,7 +109,7 @@
"settings.api.title" = "API";
"settings.language.title" = "Language";
// v0.2.1: translation feature
"settings.translation.title" = "Translation";
"settings.translation.afterPolish" = "Polish then translate";
"settings.translation.off" = "Don't translate";
"settings.languageModels.title" = "Language & models";
"settings.localModels.title" = "On-device models";
@@ -119,6 +119,7 @@
"settings.localModels.readiness %lld %lld" = "%lld/%lld ready";
"settings.localModels.cloudPolish.title" = "Cloud polish after ASR";
"settings.localModels.cloudPolish.subtitle" = "Sends the transcript to your configured cloud LLM (DeepSeek by default) for cleanup. Enable only when iOS speech recognition struggles — noisy far-field audio, strong accents, etc. Requires a DeepSeek API key.";
"settings.localModels.cloudPolish.caption" = "Uses DeepSeek to polish transcripts in local mode.";
"settings.language.subtitle.cloud" = "Recognition language and text processing mode.";
"settings.language.subtitle.local" = "Recognition language.";
"settings.mode.title" = "Mode";
@@ -109,7 +109,7 @@
"settings.api.title" = "接口";
"settings.language.title" = "语言";
// v0.2.1: 翻译功能
"settings.translation.title" = "翻译";
"settings.translation.afterPolish" = "润色后翻译";
"settings.translation.off" = "不翻译";
"settings.languageModels.title" = "语言与模型";
"settings.localModels.title" = "本地模型";
@@ -119,6 +119,7 @@
"settings.localModels.readiness %lld %lld" = "%lld/%lld 已就绪";
"settings.localModels.cloudPolish.title" = "识别后云端润色";
"settings.localModels.cloudPolish.subtitle" = "将识别文本发送给已配置的云端大模型(默认 DeepSeek)进行润色。仅在 iOS 语音识别效果不理想时(远场、噪声、方言)开启,需提前在设置中填入 DeepSeek API Key。";
"settings.localModels.cloudPolish.caption" = "本地模式下,使用 DeepSeek 进行云端润色。";
"settings.language.subtitle.cloud" = "选择识别语言和文字处理模式。";
"settings.language.subtitle.local" = "选择识别语言。";
"settings.mode.title" = "模式";