// 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 } /// One-character Chinese token for compact chips such as「中译英」. public var chineseShort: String { switch id { case "en": return "英" case "zh-Hans": return "中" case "zh-Hant": return "繁" case "ja": return "日" case "ko": return "韩" case "fr": return "法" case "de": return "德" case "es": return "西" case "ru": return "俄" case "pt": return "葡" default: return nativeName } } /// Country-style English code for compact chips such as「To JP」. public var englishShort: String { switch id { case "en": return "EN" case "zh-Hans": return "CN" case "zh-Hant": return "TW" case "ja": return "JP" case "ko": return "KR" case "fr": return "FR" case "de": return "DE" case "es": return "ES" case "ru": return "RU" case "pt": return "PT" default: return id.uppercased() } } /// 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` 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] } }