Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +113 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The transcriptionLanguage getter silently writes a normalized value back to UserDefaults without posting transcriptionSettingsDidChange. Every other mutation to transcriptionLanguage goes through the setter, which always posts the notification so observers (e.g., the audio stack) can react. If an observer is active at the moment this one-time migration fires, it won't be told the value changed, leaving the streamed language code potentially stale until the next explicit set.

Suggested change
if normalized != value {
UserDefaults.standard.set(normalized, forKey: transcriptionLanguageKey)
}
return normalized
if normalized != value {
UserDefaults.standard.set(normalized, forKey: transcriptionLanguageKey)
NotificationCenter.default.post(name: .transcriptionSettingsDidChange, object: nil)
}
return normalized

}
set {
UserDefaults.standard.set(newValue, forKey: transcriptionLanguageKey)
UserDefaults.standard.set(Self.normalizeTranscriptionLanguageCode(newValue), forKey: transcriptionLanguageKey)
NotificationCenter.default.post(name: .transcriptionSettingsDidChange, object: nil)
}
}
Expand All @@ -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<String> = [
"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
}
Expand Down Expand Up @@ -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)"),
Expand All @@ -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"),
Expand All @@ -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<String> = [
nonisolated static let multiLanguageSupported: Set<String> = [
"en", "en-US", "en-AU", "en-GB", "en-IN", "en-NZ",
"es", "es-419",
"fr", "fr-CA",
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions desktop/Desktop/Sources/Providers/ChatToolExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
62 changes: 62 additions & 0 deletions desktop/Desktop/Tests/AssistantSettingsLanguageTests.swift
Original file line number Diff line number Diff line change
@@ -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()
}
Comment on lines +9 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 testChineseDoesNotUseAutoDetectMultiLanguageMode writes true to AssistantSettings.shared.transcriptionAutoDetect, but tearDown only removes transcriptionLanguage from UserDefaults. The transcriptionAutoDetect key is left dirty for any subsequent test that reads effectiveTranscriptionLanguage on the shared singleton. Adding transcriptionAutoDetect removal to both setUp and tearDown keeps tests independent of execution order.

Suggested change
override func setUp() {
super.setUp()
UserDefaults.standard.removeObject(forKey: languageKey)
}
override func tearDown() {
UserDefaults.standard.removeObject(forKey: languageKey)
super.tearDown()
}
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"))
}
}
Loading