Files
OSGKeyboard/OSGKeyboardShared/Models/TranslationLanguage.swift
T
rockyandClaude Opus 5 0023741455 feat(app): add crash visibility, memory relief, and clipboard reply work
Squashes the working tree that had accumulated on cloud-agent. Two
strands of work, kept in one commit at the maintainer's request.

Crash and stability (this session):

- Subscribe to MetricKit so crash, hang, CPU, and disk-write diagnostics
  are recorded on device. Keyboard-extension memory kills are the most
  likely way a user loses the keyboard mid-sentence and iOS does not
  classify them as crashes, so they never reach App Store Connect.
- Upload those reports to POST /v1/analytics/diagnostics from internal
  builds only (local Debug and TestFlight), reusing the analytics
  installation identifier and skipping entirely when analytics are off.
  App Store builds keep everything on device.
- Shed the Chinese engine, English lexicon, and in-flight pipeline work
  at the 40 MiB and 48 MiB budget thresholds instead of waiting for a
  system memory warning that jetsam often preempts.
- Install audio taps with the input node's live format through one
  shared adaptive resampler, so an audio-route change mid-recording no
  longer raises an uncatchable format-mismatch exception. Covers the
  dictation preview and the Mac recorder, which still used the pattern
  the Flow capture path had already moved away from.
- Harden the librime bridge: never pass a nil string across a nonnull
  boundary when a user dictionary holds malformed UTF-8, null-check the
  engine API vector, and distrust a candidate count without its array.
- Guard the expanded candidate grid against a list that shrinks while
  the collection view is still laying out against the previous count.
- Resolve localized text without the App Group so the screen shown when
  that container is missing renders its message instead of crashing on
  launch, which is the one situation it exists to handle.
- Gate CI on the App Group and Keychain wiring that the keyboard
  extension hard-depends on, with a self-test proving the check fails.

Clipboard, reply, and semantics (pre-existing working tree):

- Unify the reply center, refresh keyboard AI features, and add native
  event and reminder exporters.
- Expand clipboard semantic routing with authored corpora and a
  product-blind holdout, plus the scripts that build and validate them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:31:29 -07:00

158 lines
6.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 menu. 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
}
/// True when this target is a Chinese script (简体 or 繁體).
public var isChineseScript: Bool {
id == "zh-Hans" || id == "zh-Hant"
}
}
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 and keyboard menu stay a plain `String`.
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 / menu 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]
}
}
/// Resolves the device's first preferred language for clipboard translation.
/// This intentionally ignores the optional post-dictation translation target.
public enum SystemLanguageResolver {
public static func primaryIdentifier(
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
normalizedIdentifier(
preferredLanguages.first ?? Locale.autoupdatingCurrent.identifier
)
}
public static func promptLanguageName(
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
let identifier = primaryIdentifier(preferredLanguages: preferredLanguages)
if let known = TranslationLanguageCatalog.all.first(where: { $0.id == identifier }) {
return known.promptLanguageName
}
return Locale(identifier: "en").localizedString(forIdentifier: identifier)
?? identifier
}
public static func displayLanguageName(
uiLanguage: AppUILanguage,
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
let identifier = primaryIdentifier(preferredLanguages: preferredLanguages)
let displayLocale = Locale(identifier: uiLanguage.resolvedLanguageCode())
return displayLocale.localizedString(forIdentifier: identifier)
?? promptLanguageName(preferredLanguages: preferredLanguages)
}
public static func isSameLanguage(
sourceIdentifier: String,
targetIdentifier: String
) -> Bool {
let source = normalizedIdentifier(sourceIdentifier)
let target = normalizedIdentifier(targetIdentifier)
if source.hasPrefix("zh"), target.hasPrefix("zh") {
let sourceScript = chineseScript(in: source)
let targetScript = chineseScript(in: target)
return sourceScript == nil || targetScript == nil || sourceScript == targetScript
}
return source == target
}
private static func normalizedIdentifier(_ identifier: String) -> String {
let language = Locale.Language(identifier: identifier)
guard let rawCode = language.languageCode?.identifier else {
return identifier.lowercased()
}
let code = rawCode.lowercased()
guard code == "zh" || code == "yue" else { return code }
let script = language.script?.identifier.lowercased()
let region = Locale(identifier: identifier).region?.identifier.uppercased()
if script == "hant" || ["HK", "MO", "TW"].contains(region) {
return "zh-Hant"
}
if script == "hans" || ["CN", "MY", "SG"].contains(region) {
return "zh-Hans"
}
return "zh"
}
private static func chineseScript(in identifier: String) -> String? {
let normalized = identifier.lowercased()
if normalized.contains("hant") { return "Hant" }
if normalized.contains("hans") { return "Hans" }
return nil
}
}