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
@@ -76,6 +76,11 @@ public struct KeyboardRootView: View {
LocaleChip(localeId: state.localeId) { newId in
state.setLocale(newId)
}
// v0.2.1: translation chip sits next to the locale picker
// and doubles as both the on/off switch and the target-
// language picker (Menu pattern matches LocaleChip so the
// top bar stays visually consistent).
TranslationChip(state: state)
Spacer(minLength: 0)
StatusBadge(phase: state.phase, onDeviceSupported: state.onDeviceSupported)
Button(action: state.openSettings) {