diff --git a/desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift b/desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift index 57dd5283c67..13b0b0c1732 100644 --- a/desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift +++ b/desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift @@ -1143,7 +1143,7 @@ struct SettingsContentView: View { .foregroundColor(OmiColors.textTertiary) Text( - "Single language mode supports 42 languages including Ukrainian, Russian, and more." + "Single language mode supports \(AssistantSettings.supportedLanguages.count) languages including Chinese, Ukrainian, Russian, and more." ) .scaledFont(size: 11) .foregroundColor(OmiColors.textTertiary) @@ -6734,14 +6734,16 @@ struct SettingsContentView: View { AssistantSettings.shared.transcriptionVocabulary = transcription.vocabulary // Sync backend language to local if different (backend is source of truth for language) - if !language.language.isEmpty && language.language != transcriptionLanguage { - transcriptionLanguage = language.language - AssistantSettings.shared.transcriptionLanguage = language.language + let normalizedLanguage = AssistantSettings.normalizeTranscriptionLanguageCode(language.language) + if !language.language.isEmpty && normalizedLanguage != transcriptionLanguage { + transcriptionLanguage = normalizedLanguage + AssistantSettings.shared.transcriptionLanguage = normalizedLanguage } // Sync single language mode from backend (inverted to auto-detect) // Only update if we got a valid response and it differs - let backendAutoDetect = !transcription.singleLanguageMode + let backendAutoDetect = + !transcription.singleLanguageMode && AssistantSettings.supportsAutoDetect(normalizedLanguage) if backendAutoDetect != transcriptionAutoDetect { transcriptionAutoDetect = backendAutoDetect AssistantSettings.shared.transcriptionAutoDetect = backendAutoDetect diff --git a/desktop/Desktop/Sources/ProactiveAssistants/Services/AssistantSettings.swift b/desktop/Desktop/Sources/ProactiveAssistants/Services/AssistantSettings.swift index 38d8bf229fc..35580486d9b 100644 --- a/desktop/Desktop/Sources/ProactiveAssistants/Services/AssistantSettings.swift +++ b/desktop/Desktop/Sources/ProactiveAssistants/Services/AssistantSettings.swift @@ -108,11 +108,15 @@ class AssistantSettings { /// The language code for transcription (e.g., "en", "uk", "ru") var transcriptionLanguage: String { get { - let value = UserDefaults.standard.string(forKey: transcriptionLanguageKey) - return value ?? defaultTranscriptionLanguage + let value = UserDefaults.standard.string(forKey: transcriptionLanguageKey) ?? defaultTranscriptionLanguage + let normalized = Self.normalizeTranscriptionLanguageCode(value) + if normalized != value { + UserDefaults.standard.set(normalized, forKey: transcriptionLanguageKey) + } + return normalized } set { - UserDefaults.standard.set(newValue, forKey: transcriptionLanguageKey) + UserDefaults.standard.set(Self.normalizeTranscriptionLanguageCode(newValue), forKey: transcriptionLanguageKey) NotificationCenter.default.post(name: .transcriptionSettingsDidChange, object: nil) } } @@ -132,26 +136,8 @@ class AssistantSettings { /// If auto-detect is enabled and the language supports multi-language mode, returns "multi" /// Otherwise returns the specific language code var effectiveTranscriptionLanguage: String { - if transcriptionAutoDetect { - // Languages that support multi-language detection in DeepGram Nova-3 - let multiLanguageSupported: Set = [ - "en", "en-US", "en-AU", "en-GB", "en-IN", "en-NZ", - "es", "es-419", - "fr", "fr-CA", - "de", - "hi", - "ru", - "pt", "pt-BR", "pt-PT", - "ja", - "it", - "nl" - ] - - // If the selected language supports multi-language mode, use "multi" - // Otherwise fall back to single language (e.g., Ukrainian doesn't support multi) - if multiLanguageSupported.contains(transcriptionLanguage) { - return "multi" - } + if transcriptionAutoDetect && Self.supportsAutoDetect(transcriptionLanguage) { + return "multi" } return transcriptionLanguage } @@ -224,8 +210,28 @@ class AssistantSettings { // MARK: - Supported Languages - /// All languages supported by DeepGram Nova-3 for single-language transcription - static let supportedLanguages: [(code: String, name: String)] = [ + /// Canonical backend-supported DeepGram Nova-3 language options for single-language transcription. + nonisolated static let supportedLanguages: [(code: String, name: String)] = [ + ("ar", "Arabic"), + ("ar-AE", "Arabic (United Arab Emirates)"), + ("ar-SA", "Arabic (Saudi Arabia)"), + ("ar-QA", "Arabic (Qatar)"), + ("ar-KW", "Arabic (Kuwait)"), + ("ar-SY", "Arabic (Syria)"), + ("ar-LB", "Arabic (Lebanon)"), + ("ar-PS", "Arabic (Palestine)"), + ("ar-JO", "Arabic (Jordan)"), + ("ar-EG", "Arabic (Egypt)"), + ("ar-SD", "Arabic (Sudan)"), + ("ar-TD", "Arabic (Chad)"), + ("ar-MA", "Arabic (Morocco)"), + ("ar-DZ", "Arabic (Algeria)"), + ("ar-TN", "Arabic (Tunisia)"), + ("ar-IQ", "Arabic (Iraq)"), + ("ar-IR", "Arabic (Iran)"), + ("be", "Belarusian"), + ("bn", "Bengali"), + ("bs", "Bosnian"), ("en", "English"), ("en-US", "English (US)"), ("en-GB", "English (UK)"), @@ -234,25 +240,36 @@ class AssistantSettings { ("en-NZ", "English (New Zealand)"), ("bg", "Bulgarian"), ("ca", "Catalan"), + ("zh-CN", "Chinese (Simplified)"), + ("zh-HK", "Chinese (Hong Kong)"), + ("zh-TW", "Chinese (Taiwan)"), ("cs", "Czech"), ("da", "Danish"), + ("da-DK", "Danish (Denmark)"), ("nl", "Dutch"), ("nl-BE", "Dutch (Belgium)"), ("et", "Estonian"), + ("fa", "Persian"), ("fi", "Finnish"), ("fr", "French"), ("fr-CA", "French (Canada)"), ("de", "German"), ("de-CH", "German (Switzerland)"), ("el", "Greek"), + ("he", "Hebrew"), ("hi", "Hindi"), + ("hr", "Croatian"), ("hu", "Hungarian"), ("id", "Indonesian"), ("it", "Italian"), ("ja", "Japanese"), + ("kn", "Kannada"), ("ko", "Korean"), + ("ko-KR", "Korean (South Korea)"), ("lv", "Latvian"), ("lt", "Lithuanian"), + ("mk", "Macedonian"), + ("mr", "Marathi"), ("ms", "Malay"), ("no", "Norwegian"), ("pl", "Polish"), @@ -262,16 +279,25 @@ class AssistantSettings { ("ro", "Romanian"), ("ru", "Russian"), ("sk", "Slovak"), + ("sl", "Slovenian"), + ("sr", "Serbian"), ("es", "Spanish"), ("es-419", "Spanish (Latin America)"), ("sv", "Swedish"), + ("sv-SE", "Swedish (Sweden)"), + ("ta", "Tamil"), + ("te", "Telugu"), + ("th", "Thai"), + ("th-TH", "Thai (Thailand)"), + ("tl", "Tagalog"), ("tr", "Turkish"), ("uk", "Ukrainian"), + ("ur", "Urdu"), ("vi", "Vietnamese"), ] /// Languages that support multi-language (auto-detect) mode in DeepGram Nova-3 - static let multiLanguageSupported: Set = [ + nonisolated static let multiLanguageSupported: Set = [ "en", "en-US", "en-AU", "en-GB", "en-IN", "en-NZ", "es", "es-419", "fr", "fr-CA", @@ -285,9 +311,54 @@ class AssistantSettings { ] /// Check if a language supports auto-detect mode - static func supportsAutoDetect(_ languageCode: String) -> Bool { - return multiLanguageSupported.contains(languageCode) + nonisolated static func supportsAutoDetect(_ languageCode: String) -> Bool { + return multiLanguageSupported.contains(normalizeTranscriptionLanguageCode(languageCode)) + } + + nonisolated static func normalizeTranscriptionLanguageCode(_ rawValue: String) -> String { + let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return "en" } + + let normalizedSeparator = trimmed.replacingOccurrences(of: "_", with: "-") + let lookupKey = normalizedSeparator.lowercased() + + if let alias = transcriptionLanguageAliases[lookupKey] { + return alias + } + + if let supported = supportedLanguages.first(where: { + $0.code.compare(normalizedSeparator, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame + }) { + return supported.code + } + + return normalizedSeparator } + + /// Normalizes legacy, backend, and user-entered aliases into codes accepted by `/v4/listen`. + nonisolated private static let transcriptionLanguageAliases: [String: String] = [ + "br": "pt-BR", + "chinese": "zh-CN", + "chinese simplified": "zh-CN", + "chinese (simplified)": "zh-CN", + "mandarin": "zh-CN", + "mandarin chinese": "zh-CN", + "pt-br": "pt-BR", + "simplified chinese": "zh-CN", + "zh": "zh-CN", + "zh-cn": "zh-CN", + "zh-hans": "zh-CN", + "zh-tw": "zh-TW", + "zh-hant": "zh-TW", + "zh-hk": "zh-HK", + "中文": "zh-CN", + "普通话": "zh-CN", + "汉语": "zh-CN", + "国语": "zh-CN", + "简体中文": "zh-CN", + "繁体中文": "zh-TW", + "粤语": "zh-HK", + ] } // MARK: - Notification Names diff --git a/desktop/Desktop/Sources/Providers/ChatToolExecutor.swift b/desktop/Desktop/Sources/Providers/ChatToolExecutor.swift index d17ad74be21..25104e79188 100644 --- a/desktop/Desktop/Sources/Providers/ChatToolExecutor.swift +++ b/desktop/Desktop/Sources/Providers/ChatToolExecutor.swift @@ -1087,13 +1087,14 @@ class ChatToolExecutor { var results: [String] = [] if let language = args["language"] as? String, !language.isEmpty { - AssistantSettings.shared.transcriptionLanguage = language - let supportsMulti = AssistantSettings.supportsAutoDetect(language) + let normalizedLanguage = AssistantSettings.normalizeTranscriptionLanguageCode(language) + AssistantSettings.shared.transcriptionLanguage = normalizedLanguage + let supportsMulti = AssistantSettings.supportsAutoDetect(normalizedLanguage) AssistantSettings.shared.transcriptionAutoDetect = supportsMulti Task { - _ = try? await APIClient.shared.updateUserLanguage(language) + _ = try? await APIClient.shared.updateUserLanguage(normalizedLanguage) } - results.append("Language set to \(language)") + results.append("Language set to \(normalizedLanguage)") } if let name = args["name"] as? String, !name.isEmpty { diff --git a/desktop/Desktop/Tests/AssistantSettingsLanguageTests.swift b/desktop/Desktop/Tests/AssistantSettingsLanguageTests.swift new file mode 100644 index 00000000000..aead810bce0 --- /dev/null +++ b/desktop/Desktop/Tests/AssistantSettingsLanguageTests.swift @@ -0,0 +1,62 @@ +import XCTest +@testable import Omi_Computer + +@MainActor +final class AssistantSettingsLanguageTests: XCTestCase { + private let languageKey = "transcriptionLanguage" + private let autoDetectKey = "transcriptionAutoDetect" + + override func setUp() { + super.setUp() + UserDefaults.standard.removeObject(forKey: languageKey) + UserDefaults.standard.removeObject(forKey: autoDetectKey) + } + + override func tearDown() { + UserDefaults.standard.removeObject(forKey: languageKey) + UserDefaults.standard.removeObject(forKey: autoDetectKey) + super.tearDown() + } + + func testNormalizesChineseAliasesToDeepgramCode() { + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("chinese"), "zh-CN") + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("zh"), "zh-CN") + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("zh_Hans"), "zh-CN") + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("中文"), "zh-CN") + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("zh-Hant"), "zh-TW") + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("粤语"), "zh-HK") + } + + func testPersistedAliasIsNormalizedAndWrittenBackOnRead() { + UserDefaults.standard.set("chinese", forKey: languageKey) + + XCTAssertEqual(AssistantSettings.shared.transcriptionLanguage, "zh-CN") + XCTAssertEqual(UserDefaults.standard.string(forKey: languageKey), "zh-CN") + } + + func testChineseDoesNotUseAutoDetectMultiLanguageMode() { + AssistantSettings.shared.transcriptionLanguage = "chinese" + AssistantSettings.shared.transcriptionAutoDetect = true + + XCTAssertEqual(AssistantSettings.shared.effectiveTranscriptionLanguage, "zh-CN") + XCTAssertFalse(AssistantSettings.supportsAutoDetect("chinese")) + } + + func testBrazilianPortugueseAliasIsSupported() { + XCTAssertEqual(AssistantSettings.normalizeTranscriptionLanguageCode("br"), "pt-BR") + XCTAssertEqual(AssistantSettings.shared.transcriptionLanguage, "en") + XCTAssertTrue(AssistantSettings.supportsAutoDetect("br")) + } + + func testDesktopLanguagePickerIncludesBackendSupportedLanguages() { + let languageCodes = Set(AssistantSettings.supportedLanguages.map(\.code)) + + XCTAssertTrue(languageCodes.contains("zh-CN")) + XCTAssertTrue(languageCodes.contains("zh-HK")) + XCTAssertTrue(languageCodes.contains("zh-TW")) + XCTAssertTrue(languageCodes.contains("ar")) + XCTAssertTrue(languageCodes.contains("bn")) + XCTAssertTrue(languageCodes.contains("ta")) + XCTAssertTrue(languageCodes.contains("ur")) + } +}