diff --git a/Apps/Stratix/Sources/Stratix/Features/CloudLibrary/Settings/CloudLibrarySettingsStreamPane.swift b/Apps/Stratix/Sources/Stratix/Features/CloudLibrary/Settings/CloudLibrarySettingsStreamPane.swift index 6638283..a284b37 100644 --- a/Apps/Stratix/Sources/Stratix/Features/CloudLibrary/Settings/CloudLibrarySettingsStreamPane.swift +++ b/Apps/Stratix/Sources/Stratix/Features/CloudLibrary/Settings/CloudLibrarySettingsStreamPane.swift @@ -3,6 +3,7 @@ // import SwiftUI +import StratixModels extension CloudLibrarySettingsView { var streamPane: some View { @@ -14,6 +15,17 @@ extension CloudLibrarySettingsView { CloudLibraryPickerRow(title: "Client Profile", selection: streamBinding(\.clientProfileOSName), options: ["Auto", "Android", "Windows", "Tizen"]) CloudLibraryPickerRow(title: "Resolution", selection: streamBinding(\.preferredResolution), options: ["720p", "1080p", "1440p"]) CloudLibraryPickerRow(title: "Frame Rate", selection: streamBinding(\.preferredFPS), options: ["30", "60"]) + CloudLibraryPickerRow( + title: "Preferred Game Language", + selection: Binding( + get: { streamBinding(\.preferredGameLanguage).wrappedValue.displayName }, + set: { newValue in + let language = SupportedGameLanguage.allCases.first { $0.displayName == newValue } ?? .systemDefault + streamBinding(\.preferredGameLanguage).wrappedValue = language + } + ), + options: SupportedGameLanguage.allCases.map(\.displayName) + ) CloudLibraryPickerRow(title: "Stats HUD Position", selection: streamBinding(\.statsHUDPosition), options: ["topRight", "topLeft", "bottomRight", "bottomLeft"]) CloudLibrarySliderRow(title: "Bitrate Cap", value: streamBinding(\.bitrateCapMbps), range: 0...100, formatter: bitrateText, step: 4) CloudLibraryToggleRow(title: "HDR Preferred", subtitle: "Enable HDR metadata when available", isOn: streamBinding(\.hdrEnabled)) diff --git a/Packages/StratixCore/Sources/StratixCore/SettingsStore.swift b/Packages/StratixCore/Sources/StratixCore/SettingsStore.swift index fc2d510..a7fa5c3 100644 --- a/Packages/StratixCore/Sources/StratixCore/SettingsStore.swift +++ b/Packages/StratixCore/Sources/StratixCore/SettingsStore.swift @@ -63,7 +63,13 @@ public final class SettingsStore { } public struct StreamSettings: Sendable, Equatable { - public var locale: String + public var preferredGameLanguage: SupportedGameLanguage + public var locale: String { + get { preferredGameLanguage.localeCode } + set { + preferredGameLanguage = SupportedGameLanguage(rawValue: newValue) ?? .systemDefault + } + } public var preferIPv6: Bool public var preferredRegionID: String public var statsHUDPosition: String @@ -108,9 +114,16 @@ public final class SettingsStore { colorRange: String = "Auto", safeAreaPercent: Double = 100.0, stereoAudio: Bool = false, - chatChannelEnabled: Bool = false + chatChannelEnabled: Bool = false, + preferredGameLanguage: SupportedGameLanguage? = nil ) { - self.locale = locale + if let preferredGameLanguage { + self.preferredGameLanguage = preferredGameLanguage + } else if let mappedLanguage = SupportedGameLanguage(rawValue: locale) { + self.preferredGameLanguage = mappedLanguage + } else { + self.preferredGameLanguage = .systemDefault + } self.preferIPv6 = preferIPv6 self.preferredRegionID = preferredRegionID self.statsHUDPosition = statsHUDPosition @@ -235,6 +248,7 @@ public final class SettingsStore { static let focusPrefetchEnabled = "guide.focus_prefetch_enabled" static let locale = "stratix.stream.locale" + static let preferredGameLanguage = "stratix.stream.preferredGameLanguage" static let preferIPv6 = "stratix.stream.preferIPv6" static let preferredRegionID = "stratix.stream.preferredRegionId" static let statsHUDPosition = "stratix.stream.statsHUDPosition" @@ -522,6 +536,7 @@ public final class SettingsStore { private func persist(_ value: StreamSettings, oldValue: StreamSettings) { guard !isReloadingFromDefaults, value != oldValue else { return } defaults.set(value.locale, forKey: Key.locale) + defaults.set(value.preferredGameLanguage.rawValue, forKey: Key.preferredGameLanguage) defaults.set(value.preferIPv6, forKey: Key.preferIPv6) defaults.set(value.preferredRegionID, forKey: Key.preferredRegionID) defaults.set(value.statsHUDPosition, forKey: Key.statsHUDPosition) @@ -600,9 +615,20 @@ public final class SettingsStore { ) } + private nonisolated static func resolvePreferredGameLanguage(from defaults: UserDefaults) -> SupportedGameLanguage { + if let rawValue = defaults.string(forKey: Key.preferredGameLanguage), + let language = SupportedGameLanguage(rawValue: rawValue) { + return language + } + + let legacyLocale = string(defaults, Key.locale, fallback: "en-US") + return SupportedGameLanguage(rawValue: legacyLocale) ?? .systemDefault + } + private nonisolated static func readStream(from defaults: UserDefaults) -> StreamSettings { - StreamSettings( - locale: string(defaults, Key.locale, fallback: "en-US"), + let preferredGameLanguage = resolvePreferredGameLanguage(from: defaults) + return StreamSettings( + locale: preferredGameLanguage.localeCode, preferIPv6: bool(defaults, Key.preferIPv6, fallback: false), preferredRegionID: string(defaults, Key.preferredRegionID, fallback: ""), statsHUDPosition: string(defaults, Key.statsHUDPosition, fallback: "topRight"), @@ -623,7 +649,8 @@ public final class SettingsStore { colorRange: string(defaults, Key.colorRange, fallback: "Auto"), safeAreaPercent: double(defaults, Key.safeAreaPercent, fallback: 100.0), stereoAudio: bool(defaults, Key.stereoAudio, fallback: false), - chatChannelEnabled: bool(defaults, Key.chatChannelEnabled, fallback: false) + chatChannelEnabled: bool(defaults, Key.chatChannelEnabled, fallback: false), + preferredGameLanguage: preferredGameLanguage ) } diff --git a/Packages/StratixCore/Tests/StratixCoreTests/SettingsStoreTests.swift b/Packages/StratixCore/Tests/StratixCoreTests/SettingsStoreTests.swift index bbb496c..06747ac 100644 --- a/Packages/StratixCore/Tests/StratixCoreTests/SettingsStoreTests.swift +++ b/Packages/StratixCore/Tests/StratixCoreTests/SettingsStoreTests.swift @@ -199,4 +199,32 @@ struct SettingsStoreTests { #expect(suite.string(forKey: "stratix.debug.upscaling_floor_behavior") == "metalFloor") #expect(store.diagnostics.upscalingFloorBehavior == .metalFloor) } + + @Test + func stream_preferredGameLanguageRoundTrips() { + let suiteName = "SettingsStoreTests.stream.preferredGameLanguageRoundTrips" + let suite = UserDefaults(suiteName: suiteName)! + suite.removePersistentDomain(forName: suiteName) + + let store = SettingsStore(defaults: suite) + store.stream.preferredGameLanguage = .ruRU + + #expect(suite.string(forKey: "stratix.stream.preferredGameLanguage") == "ru-RU") + #expect(suite.string(forKey: "stratix.stream.locale") == "ru-RU") + #expect(store.stream.preferredGameLanguage == .ruRU) + #expect(store.stream.locale == "ru-RU") + } + + @Test + func stream_migratesLegacyLocaleToPreferredGameLanguage() { + let suiteName = "SettingsStoreTests.stream.migratesLegacyLocaleToPreferredGameLanguage" + let suite = UserDefaults(suiteName: suiteName)! + suite.removePersistentDomain(forName: suiteName) + suite.set("de-DE", forKey: "stratix.stream.locale") + + let store = SettingsStore(defaults: suite) + + #expect(store.stream.preferredGameLanguage == .deDE) + #expect(store.stream.locale == "de-DE") + } } diff --git a/Packages/StratixModels/Sources/StratixModels/Streaming/SupportedGameLanguage.swift b/Packages/StratixModels/Sources/StratixModels/Streaming/SupportedGameLanguage.swift new file mode 100644 index 0000000..599d99c --- /dev/null +++ b/Packages/StratixModels/Sources/StratixModels/Streaming/SupportedGameLanguage.swift @@ -0,0 +1,47 @@ +// +// SupportedGameLanguage.swift +// StratixModels +// + +import Foundation + +/// Supported xCloud preferred game language values. +public enum SupportedGameLanguage: String, CaseIterable, Identifiable, Codable, Sendable { + case systemDefault = "system" + case enUS = "en-US" + case ruRU = "ru-RU" + case deDE = "de-DE" + case frFR = "fr-FR" + case esES = "es-ES" + case ptBR = "pt-BR" + case jaJP = "ja-JP" + case koKR = "ko-KR" + case zhCN = "zh-CN" + case itIT = "it-IT" + case plPL = "pl-PL" + case trTR = "tr-TR" + + public var id: String { rawValue } + + public var displayName: String { + switch self { + case .systemDefault: return "System Default (Account Region)" + case .enUS: return "English (United States)" + case .ruRU: return "Russian" + case .deDE: return "German" + case .frFR: return "French" + case .esES: return "Spanish (Spain)" + case .ptBR: return "Portuguese (Brazil)" + case .jaJP: return "Japanese" + case .koKR: return "Korean" + case .zhCN: return "Chinese (Simplified)" + case .itIT: return "Italian" + case .plPL: return "Polish" + case .trTR: return "Turkish" + } + } + + public var localeCode: String { + self == .systemDefault ? "en-US" : rawValue + } +}