feat(translation): add post-ASR translation mode for cloud engine

Adds an opt-in translation pipeline that reuses the existing
PolishingService + LLMClient + AppGroupStore chain. Translation is
implemented as a new PolishMode (.translate(targetLocaleId:)); all
existing call sites are unchanged.

Settings:
- New TranslationPickerRow in the language tab (Toggle + 10-locale
  picker: en/zh-Hans/zh-Hant/ja/ko/fr/de/es/ru/pt), persisted to the
  App Group so the keyboard extension can read it during live dictation.
- 5 new strings per language (en + zh-Hans).

Keyboard:
- New TranslationChip on the top bar to the right of LocaleChip;
  same Menu pattern, lets users toggle or quickly switch target
  language without leaving the keyboard.
- PolishingService dispatches .translate with a parameterised prompt
  (en/zh variants selected by provider id); PolishingService.error
  gains a translationNotAvailable case so local-engine users get a
  clear inline warning when the toggle is on but cloud is off.
- 6 new strings per language (en + zh-Hans) for the chip + banner.

Local engine policy:
- Translation is cloud-only by design (local engine stays ASR-only
  to honour the no-roundtrip promise). Chip shows a 'cloud required'
  state and raw transcript still inserts on failure — no data loss.

Build:
- OSGKeyboardShared adds TranslationLanguage enum (10 locales) and
  TranslationPrompt factory.
- 4 new files, 9 modified. xcodebuild scheme=OSGKeyboard
  config=Debug destination=iPhone 17 Simulator: BUILD SUCCEEDED
  (0 warning, 0 error).

Also pins DEVELOPMENT_TEAM in project.yml for TestFlight uploads
(3 targets; Team X329MZU23S).
This commit is contained in:
2026-06-25 12:45:44 +08:00
parent dc9697bf3d
commit deddb49d56
17 changed files with 620 additions and 6 deletions
@@ -35,6 +35,12 @@ public struct AppGroupPersistor {
state.mode = .polish
state.engineMode = store.engineMode
state.localASRBackend = store.localASRBackend
// v0.2.1: translation toggle + target locale. Read once at
// hydration; `refreshRuntimeFlags` keeps them in sync while the
// keyboard stays open so a Settings change shows up without a
// re-present cycle.
state.translationEnabled = store.translationEnabled
state.translationTargetLocaleId = store.translationTargetLocaleId
// v0.2.0: iOS `SpeechAnalyzer` is always ready; mirror that
// into the State flags so downstream consumers see the same
// shape they did when the previous Qwen3 stack reported "ready".
@@ -75,6 +81,11 @@ public struct AppGroupPersistor {
let store = AppGroupStore()
state.engineMode = store.engineMode
state.localASRBackend = store.localASRBackend
// v0.2.1: keep translation state in sync with the host app so the
// chip on the keyboard reflects the latest value without a re-
// present cycle.
state.translationEnabled = store.translationEnabled
state.translationTargetLocaleId = store.translationTargetLocaleId
// v0.2.0: iOS `SpeechAnalyzer` is always ready. Keep these
// toggles here so the keyboard UI doesn't flicker if the host
// app briefly clears them while refactoring.
@@ -105,4 +116,17 @@ public struct AppGroupPersistor {
guard AppGroup.isAvailable else { return }
AppGroupStore().setLocalASRBackend(localASRBackend)
}
/// v0.2.1: persist translation toggle. Wired through the
/// `KeyboardViewController.setTranslation` action hook.
public func persist(translationEnabled: Bool) {
guard AppGroup.isAvailable else { return }
AppGroupStore().setTranslationEnabled(translationEnabled)
}
/// v0.2.1: persist translation target locale id (e.g. `"en"`).
public func persist(translationTargetLocaleId: String) {
guard AppGroup.isAvailable else { return }
AppGroupStore().setTranslationTargetLocaleId(translationTargetLocaleId)
}
}