Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

nonisolated enum AppLanguageSelection {
static func availableLocalizations(from localizations: [String]) -> [String] {
var seen = Set<String>()
return localizations.compactMap { localization in
let normalized = normalizedIdentifier(localization)
guard !normalized.isEmpty, normalized != "Base" else {
return nil
}
let key = normalized.lowercased()
guard seen.insert(key).inserted else {
return nil
}
return normalized
}
}

static func resolvedSelection(
for storedLanguage: String,
availableLocalizations: [String],
preferredLanguages: [String] = Locale.preferredLanguages
) -> String {
let available = self.availableLocalizations(from: availableLocalizations)
let normalizedLanguage = normalizedIdentifier(storedLanguage)
guard !available.isEmpty else {
return normalizedLanguage
}

if let exactMatch = match(normalizedLanguage, in: available) {
return exactMatch
}

let preferences = normalizedLanguage.isEmpty
? preferredLanguages.map(normalizedIdentifier).filter { !$0.isEmpty }
: [normalizedLanguage]

if let preferred = Bundle.preferredLocalizations(from: available, forPreferences: preferences).first,
let matchedPreferred = match(preferred, in: available)
{
return matchedPreferred
}

if let english = match("en", in: available) {
return english
}

return available[0]
}

static func displayName(for language: String, locale: Locale = .current) -> String {
let normalized = normalizedIdentifier(language)
guard !normalized.isEmpty else {
return ""
}
return locale.localizedString(forIdentifier: normalized) ?? normalized
}

private static func normalizedIdentifier(_ identifier: String) -> String {
identifier
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "_", with: "-")
}

private static func match(_ language: String, in available: [String]) -> String? {
guard !language.isEmpty else {
return nil
}
return available.first { $0.caseInsensitiveCompare(language) == .orderedSame }
}
}
24 changes: 17 additions & 7 deletions macos/OpenBridge/Interface/Settings/AppearanceSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import SwiftUI
struct AppearanceSettingsView: View {
@Environment(SettingsManager.self) private var settingsManager

private var languages = Bundle.main.localizations.filter { $0 != "Base" }
private var languages: [String] {
AppLanguageSelection.availableLocalizations(from: Bundle.main.localizations)
}

var body: some View {
@Bindable var settingsManager = settingsManager
Expand Down Expand Up @@ -129,18 +131,26 @@ extension AppearanceSettingsView {
)
}

@ViewBuilder
private var languageSetting: some View {
@Bindable var settingsManager = settingsManager
Picker("Language", selection: $settingsManager.language) {
Picker("Language", selection: languageSelection) {
ForEach(languages, id: \.self) { code in
Text(displayName(for: code)).tag(code)
Text(AppLanguageSelection.displayName(for: code)).tag(code)
}
}
}

private func displayName(for code: String) -> String {
Locale.current.localizedString(forIdentifier: code) ?? code
private var languageSelection: Binding<String> {
Binding(
get: {
AppLanguageSelection.resolvedSelection(
for: settingsManager.language,
availableLocalizations: languages
)
},
set: { language in
settingsManager.language = language
}
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import SwiftUI

struct GeneralSettingsView: View {
@Environment(SettingsManager.self) private var settingsManager
private var languages = Bundle.main.localizations.filter { $0 != "Base" }
private var languages: [String] {
AppLanguageSelection.availableLocalizations(from: Bundle.main.localizations)
}

@State private var cachedLaunchAtLogin: Bool = false

Expand Down Expand Up @@ -160,18 +162,26 @@ extension GeneralSettingsView {
)
}

@ViewBuilder
private var languageSetting: some View {
@Bindable var settingsManager = settingsManager
Picker("Language", selection: $settingsManager.language) {
Picker("Language", selection: languageSelection) {
ForEach(languages, id: \.self) { code in
Text(displayName(for: code)).tag(code)
Text(AppLanguageSelection.displayName(for: code)).tag(code)
}
}
}

private func displayName(for code: String) -> String {
Locale.current.localizedString(forIdentifier: code) ?? code
private var languageSelection: Binding<String> {
Binding(
get: {
AppLanguageSelection.resolvedSelection(
for: settingsManager.language,
availableLocalizations: languages
)
},
set: { language in
settingsManager.language = language
}
)
}
}

Expand Down
55 changes: 55 additions & 0 deletions macos/OpenBridgeUnitTests/Suites/AppLanguageSelectionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
@testable import OpenBridge
import Testing

struct AppLanguageSelectionTests {
@Test
func `available localizations remove base and preserve app option identifiers`() {
let localizations = AppLanguageSelection.availableLocalizations(from: ["Base", "en", "zh_Hans", "en"])

#expect(localizations == ["en", "zh-Hans"])
}

@Test
func `regional stored language resolves to supported picker option`() {
let available = ["en", "zh-Hans"]

#expect(
AppLanguageSelection.resolvedSelection(
for: "zh-Hans-CN",
availableLocalizations: available,
preferredLanguages: []
) == "zh-Hans"
)
#expect(
AppLanguageSelection.resolvedSelection(
for: "en-US",
availableLocalizations: available,
preferredLanguages: []
) == "en"
)
}

@Test
func `empty stored language resolves to preferred supported picker option`() {
let resolved = AppLanguageSelection.resolvedSelection(
for: "",
availableLocalizations: ["en", "zh-Hans"],
preferredLanguages: ["zh-Hans-CN"]
)

#expect(resolved == "zh-Hans")
#expect(!AppLanguageSelection.displayName(for: resolved, locale: Locale(identifier: "en_US_POSIX")).isEmpty)
}

@Test
func `unsupported stored language falls back to a visible supported option`() {
let resolved = AppLanguageSelection.resolvedSelection(
for: "es-MX",
availableLocalizations: ["en", "zh-Hans"],
preferredLanguages: []
)

#expect(resolved == "en")
}
}
Loading