diff --git a/Sources/MacOptimizer/App/AppState.swift b/Sources/MacOptimizer/App/AppState.swift index 26185d1..9862aed 100644 --- a/Sources/MacOptimizer/App/AppState.swift +++ b/Sources/MacOptimizer/App/AppState.swift @@ -354,12 +354,14 @@ public final class AppState: ObservableObject { processNames: runningProcesses.prefix(4).map { "\($0.name) (PID: \($0.pid))" }, includeProcesses: UserDefaults.standard.bool(forKey: "MacOptimizer_IncludeRunningAppNames") ) - + let localSnapshot = LocalHeuristicProvider.SystemSnapshot(hardware: hardwareInfo, memory: memoryStats, cpu: cpuStats, disk: diskStats) + Task { let result = await AIAssistantService.shared.chatWithCopilot( userMessage: trimmed, history: self.chatMessages, systemContext: context, + localSnapshot: localSnapshot, config: self.nimConfig ) diff --git a/Sources/MacOptimizer/Core/Operations/CLICommandRunner.swift b/Sources/MacOptimizer/Core/Operations/CLICommandRunner.swift index 75927c9..99c243f 100644 --- a/Sources/MacOptimizer/Core/Operations/CLICommandRunner.swift +++ b/Sources/MacOptimizer/Core/Operations/CLICommandRunner.swift @@ -55,8 +55,13 @@ public struct CLICommandRunner { let disk = await SystemMonitorService.shared.fetchDiskStats() let batt = await SystemMonitorService.shared.fetchBatteryStats() let hw = await SystemMonitorService.shared.fetchHardwareInfo() - - print(""" + + print(statusReport(memory: mem, cpu: cpu, disk: disk, battery: batt, hardware: hw)) + } + + /// English-only status text. The CLI never uses `L10n`; model raw values are not display text. + static func statusReport(memory mem: MemoryStats, cpu: CPUStats, disk: DiskStats, battery batt: BatteryStats, hardware hw: HardwareInfo) -> String { + """ ======================================================== ⚡ MacOptimizer Pro System Status ======================================================== @@ -65,16 +70,33 @@ public struct CLICommandRunner { ⏱️ Uptime: \(hw.uptimeString) 🧠 Memory Used: \(ByteFormatter.formatMemory(mem.actualUsedBytes)) / \(ByteFormatter.formatMemory(mem.totalBytes)) (\(Int(mem.usedPercentage * 100))%) - 📊 Memory Pressure: \(mem.pressureLevel.rawValue) + 📊 Memory Pressure: \(englishLabel(for: mem.pressureLevel)) 💾 Swap Used: \(ByteFormatter.formatMemory(mem.swapUsedBytes)) / \(ByteFormatter.formatMemory(mem.swapTotalBytes)) 🔥 CPU Usage: \(String(format: "%.1f", cpu.totalUsage))% (\(cpu.physicalCores) cores) - 🌡️ Thermal State: \(cpu.thermalState.rawValue) - + 🌡️ Thermal State: \(englishLabel(for: cpu.thermalState)) + 💽 Disk: \(ByteFormatter.format(disk.usedBytes)) used / \(ByteFormatter.format(disk.freeBytes)) free 🔋 Battery: \(batt.percentage)% (\(batt.powerSource)) ======================================================== - """) + """ + } + + static func englishLabel(for level: MemoryStats.MemoryPressureLevel) -> String { + switch level { + case .normal: return "Normal" + case .warning: return "Warning" + case .critical: return "Critical" + } + } + + static func englishLabel(for state: CPUStats.ThermalState) -> String { + switch state { + case .nominal: return "Normal (Cool)" + case .fair: return "Slightly Warm" + case .serious: return "High Temperature (Throttling Risk)" + case .critical: return "Critical Temperature (Fans at Maximum)" + } } private static func runJunkClean(dryRun: Bool, includeTrash: Bool) async { diff --git a/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift b/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift index 21c2111..0d9c4bd 100644 --- a/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift +++ b/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift @@ -5,9 +5,39 @@ public struct LocalHeuristicProvider: AIProvider { public let providerId: String = "local_heuristics" public let displayName: String = L10n.string("Local Rule Engine (100% Offline & Secure)", table: .ai) public let requiresNetwork: Bool = false - - public init() {} - + + /// Telemetry shown to the user in offline replies. The English cloud snapshot is model input only. + public struct SystemSnapshot: Sendable { + public let hardware: HardwareInfo + public let memory: MemoryStats + public let cpu: CPUStats + public let disk: DiskStats + + public init(hardware: HardwareInfo, memory: MemoryStats, cpu: CPUStats, disk: DiskStats) { + self.hardware = hardware + self.memory = memory + self.cpu = cpu + self.disk = disk + } + } + + private let snapshot: SystemSnapshot? + + public init(snapshot: SystemSnapshot? = nil) { + self.snapshot = snapshot + } + + /// Localized system summary for the user (never the English cloud snapshot text). + static func localizedSummary(_ snapshot: SystemSnapshot) -> String { + [ + L10n.string("Mac model: %@ (%@), macOS version: %@", table: .ai, + snapshot.hardware.modelName, snapshot.hardware.chipName, snapshot.hardware.osVersion), + L10n.string("RAM: %lld%%", table: .ai, Int(snapshot.memory.usedPercentage * 100)), + L10n.string("CPU: %@%%, Free disk: %lld%%", table: .ai, + String(format: "%.1f", snapshot.cpu.totalUsage), Int(snapshot.disk.freePercentage * 100)) + ].joined(separator: "\n") + } + public func diagnose( memory: MemoryStats, cpu: CPUStats, @@ -106,7 +136,11 @@ public struct LocalHeuristicProvider: AIProvider { // Keyword lists cover both Turkish and English user input. if lastUserMsg.contains("ram") || lastUserMsg.contains("bellek") || lastUserMsg.contains("memory") { - return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@", table: .ai, snapshotContext) + // `snapshotContext` is the English cloud model input; the user sees the localized summary instead. + guard let snapshot else { + return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.", table: .ai) + } + return L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@", table: .ai, Self.localizedSummary(snapshot)) } else if lastUserMsg.contains("ısın") || lastUserMsg.contains("cpu") || lastUserMsg.contains("fan") || lastUserMsg.contains("heat") { return L10n.string("Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps.", table: .ai) } else if lastUserMsg.contains("temiz") || lastUserMsg.contains("disk") || lastUserMsg.contains("yer") diff --git a/Sources/MacOptimizer/Resources/en.lproj/ai.strings b/Sources/MacOptimizer/Resources/en.lproj/ai.strings index 6d288dd..5771cb6 100644 --- a/Sources/MacOptimizer/Resources/en.lproj/ai.strings +++ b/Sources/MacOptimizer/Resources/en.lproj/ai.strings @@ -108,3 +108,7 @@ "NVIDIA NIM API key is missing." = "NVIDIA NIM API key is missing."; "Invalid /chat/completions URL format." = "Invalid /chat/completions URL format."; "Could not parse the API response." = "Could not parse the API response."; +"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps." = "I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps."; +"Mac model: %@ (%@), macOS version: %@" = "Mac model: %@ (%@), macOS version: %@"; +"RAM: %lld%%" = "RAM: %lld%%"; +"CPU: %@%%, Free disk: %lld%%" = "CPU: %@%%, Free disk: %lld%%"; diff --git a/Sources/MacOptimizer/Resources/en.lproj/cleanup.strings b/Sources/MacOptimizer/Resources/en.lproj/cleanup.strings index 11f7e1b..7e5396b 100644 --- a/Sources/MacOptimizer/Resources/en.lproj/cleanup.strings +++ b/Sources/MacOptimizer/Resources/en.lproj/cleanup.strings @@ -80,3 +80,42 @@ "Copy" = "Copy"; "Duplicate File Scan Not Run" = "Duplicate File Scan Not Run"; "Start a scan to detect files with identical content in your Downloads, Documents, and Pictures folders." = "Start a scan to detect files with identical content in your Downloads, Documents, and Pictures folders."; +"User" = "User"; +"Apple Silicon" = "Apple Silicon"; +"Universal" = "Universal"; +"Intel (x86)" = "Intel (x86)"; +"Updates" = "Updates"; +"By Size" = "By Size"; +"By Name" = "By Name"; +"Installed Apps & Uninstaller" = "Installed Apps & Uninstaller"; +"Review all apps installed on your Mac, their architectures, and sizes." = "Review all apps installed on your Mac, their architectures, and sizes."; +"Scan Apps" = "Scan Apps"; +"Checking..." = "Checking..."; +"Find Updates" = "Find Updates"; +"Search by app name or Bundle ID..." = "Search by app name or Bundle ID..."; +"Sort" = "Sort"; +"Apps Not Scanned" = "Apps Not Scanned"; +"Start a scan to list installed apps and check for updates." = "Start a scan to list installed apps and check for updates."; +"Update: v%@" = "Update: v%@"; +"Update" = "Update"; +"Uninstall" = "Uninstall"; +"Uninstall Along with Leftovers" = "Uninstall Along with Leftovers"; +"Uninstalling %@" = "Uninstalling %@"; +"Close" = "Close"; +"Searching for related files and caches..." = "Searching for related files and caches..."; +"Space to Reclaim: %@" = "Space to Reclaim: %@"; +"Uninstalling..." = "Uninstalling..."; +"Delete All and Uninstall (%@)" = "Delete All and Uninstall (%@)"; +"Uninstall %@" = "Uninstall %@"; +"The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?" = "The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?"; +"Uninstall with Leftovers" = "Uninstall with Leftovers"; +"Software & App Updates" = "Software & App Updates"; +"Check for the latest versions of your installed macOS apps." = "Check for the latest versions of your installed macOS apps."; +"Check for Updates" = "Check for Updates"; +"Current: v%@" = "Current: v%@"; +"New: v%@" = "New: v%@"; +"Release Notes" = "Release Notes"; +"Updating %@ via Homebrew." = "Updating %@ via Homebrew."; +"Get Update" = "Get Update"; +"All Your Apps Are Up to Date" = "All Your Apps Are Up to Date"; +"No pending software updates were found. Use the button above to check for new versions." = "No pending software updates were found. Use the button above to check for new versions."; diff --git a/Sources/MacOptimizer/Resources/en.lproj/cleanup.stringsdict b/Sources/MacOptimizer/Resources/en.lproj/cleanup.stringsdict index 8f5e9b7..0077565 100644 --- a/Sources/MacOptimizer/Resources/en.lproj/cleanup.stringsdict +++ b/Sources/MacOptimizer/Resources/en.lproj/cleanup.stringsdict @@ -31,4 +31,19 @@ countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld one%lld Duplicate Fileother%lld Duplicate Files +%lld apps detected • Total: %@ +NSStringLocalizedFormatKey%#@count@ detected • Total: %@ +countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld +one%lld appother%lld apps + +Related Files Found (%lld items) +NSStringLocalizedFormatKeyRelated Files Found (%#@count@) +countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld +one%lld itemother%lld items + +New versions available for %lld apps! +NSStringLocalizedFormatKeyNew versions available for %#@count@! +countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld +one%lld appother%lld apps + diff --git a/Sources/MacOptimizer/Resources/tr.lproj/ai.strings b/Sources/MacOptimizer/Resources/tr.lproj/ai.strings index c76a9db..7a38399 100644 --- a/Sources/MacOptimizer/Resources/tr.lproj/ai.strings +++ b/Sources/MacOptimizer/Resources/tr.lproj/ai.strings @@ -108,3 +108,7 @@ "NVIDIA NIM API key is missing." = "NVIDIA NIM API Anahtarı eksik."; "Invalid /chat/completions URL format." = "Geçersiz /chat/completions URL formatı."; "Could not parse the API response." = "API yanıtı ayrıştırılamadı."; +"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps." = "Mac'inizin bellek durumunu inceledim. Bellek baskısını azaltmak için en yoğun uygulamaları kapatmayı deneyebilirsiniz."; +"Mac model: %@ (%@), macOS version: %@" = "Mac modeli: %@ (%@), macOS sürümü: %@"; +"RAM: %lld%%" = "RAM: %%%lld"; +"CPU: %@%%, Free disk: %lld%%" = "CPU: %%%@, Boş disk: %%%lld"; diff --git a/Sources/MacOptimizer/Resources/tr.lproj/cleanup.strings b/Sources/MacOptimizer/Resources/tr.lproj/cleanup.strings index ad76bad..d656a63 100644 --- a/Sources/MacOptimizer/Resources/tr.lproj/cleanup.strings +++ b/Sources/MacOptimizer/Resources/tr.lproj/cleanup.strings @@ -80,3 +80,42 @@ "Copy" = "Kopya"; "Duplicate File Scan Not Run" = "Yinelenen Dosya Taraması Yapılmadı"; "Start a scan to detect files with identical content in your Downloads, Documents, and Pictures folders." = "İndirilenler, Belgeler ve Resimler klasörlerindeki aynı içerikli dosyaları tespit etmek için taramayı başlatın."; +"User" = "Kullanıcı"; +"Apple Silicon" = "Apple Silicon"; +"Universal" = "Universal"; +"Intel (x86)" = "Intel (x86)"; +"Updates" = "Güncellemeler"; +"By Size" = "Boyuta Göre"; +"By Name" = "İsme Göre"; +"Installed Apps & Uninstaller" = "Yüklü Uygulamalar & Kaldırıcı"; +"Review all apps installed on your Mac, their architectures, and sizes." = "Mac'inizde kurulu tüm uygulamaları, mimarilerini ve boyutlarını inceleyin."; +"Scan Apps" = "Uygulamaları Tara"; +"Checking..." = "Denetleniyor..."; +"Find Updates" = "Güncellemeleri Bul"; +"Search by app name or Bundle ID..." = "Uygulama adı veya Bundle ID ara..."; +"Sort" = "Sırala"; +"Apps Not Scanned" = "Uygulamalar Taranmadı"; +"Start a scan to list installed apps and check for updates." = "Kurulu olan uygulamaları listelemek ve güncelleme kontrolü yapmak için taramayı başlatın."; +"Update: v%@" = "Güncelleme: v%@"; +"Update" = "Güncelle"; +"Uninstall" = "Kaldır"; +"Uninstall Along with Leftovers" = "Kalıntılarıyla Birlikte Kaldır"; +"Uninstalling %@" = "%@ Kaldırılıyor"; +"Close" = "Kapat"; +"Searching for related files and caches..." = "İlişkili dosyalar ve önbellekler aranıyor..."; +"Space to Reclaim: %@" = "Kazanılacak Alan: %@"; +"Uninstalling..." = "Kaldırılıyor..."; +"Delete All and Uninstall (%@)" = "Tümünü Sil ve Kaldır (%@)"; +"Uninstall %@" = "%@ Uygulamasını Kaldır"; +"The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?" = "Uygulama paketi Çöp Sepetine taşınacak. Seçili kalıntılar da Çöp Sepetine taşınacaktır. Devam edilsin mi?"; +"Uninstall with Leftovers" = "Kalıntılarıyla Kaldır"; +"Software & App Updates" = "Yazılım & Uygulama Güncellemeleri"; +"Check for the latest versions of your installed macOS apps." = "Yüklü macOS uygulamalarınızın en son sürümlerini kontrol edin."; +"Check for Updates" = "Güncellemeleri Denetle"; +"Current: v%@" = "Mevcut: v%@"; +"New: v%@" = "Yeni: v%@"; +"Release Notes" = "Sürüm Notları"; +"Updating %@ via Homebrew." = "%@ Homebrew üzerinden güncelleniyor."; +"Get Update" = "Güncellemeyi Al"; +"All Your Apps Are Up to Date" = "Tüm Uygulamalarınız Güncel"; +"No pending software updates were found. Use the button above to check for new versions." = "Herhangi bir bekleyen yazılım güncellemesi bulunamadı. Yeni sürümleri denetlemek için yukarıdaki butonu kullanabilirsiniz."; diff --git a/Sources/MacOptimizer/Resources/tr.lproj/cleanup.stringsdict b/Sources/MacOptimizer/Resources/tr.lproj/cleanup.stringsdict index 2b67dcb..0be9470 100644 --- a/Sources/MacOptimizer/Resources/tr.lproj/cleanup.stringsdict +++ b/Sources/MacOptimizer/Resources/tr.lproj/cleanup.stringsdict @@ -31,4 +31,19 @@ countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld one%lld Kopya Dosyaother%lld Kopya Dosya +%lld apps detected • Total: %@ +NSStringLocalizedFormatKey%#@count@ tespit edildi • Toplam: %@ +countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld +one%lld uygulamaother%lld uygulama + +Related Files Found (%lld items) +NSStringLocalizedFormatKeyBulunan İlişkili Dosyalar (%#@count@) +countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld +one%lld öğeother%lld öğe + +New versions available for %lld apps! +NSStringLocalizedFormatKey%#@count@ için yeni sürüm mevcut! +countNSStringFormatSpecTypeKeyNSStringPluralRuleTypeNSStringFormatValueTypeKeylld +one%lld uygulamaother%lld uygulama + diff --git a/Sources/MacOptimizer/Services/AIAssistantService.swift b/Sources/MacOptimizer/Services/AIAssistantService.swift index 0f819da..f23bb9b 100644 --- a/Sources/MacOptimizer/Services/AIAssistantService.swift +++ b/Sources/MacOptimizer/Services/AIAssistantService.swift @@ -4,10 +4,10 @@ public actor AIAssistantService { public static let shared = AIAssistantService() public init() {} - private func provider(for config: NIMConfig) -> any AIProvider { + private func provider(for config: NIMConfig, localSnapshot: LocalHeuristicProvider.SystemSnapshot? = nil) -> any AIProvider { guard config.isEnabled, config.providerType == .nvidiaNIM, UserDefaults.standard.bool(forKey: "MacOptimizer_NIMDisclosureAccepted"), - !config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return LocalHeuristicProvider() } + !config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return LocalHeuristicProvider(snapshot: localSnapshot) } return NvidiaNIMProvider(config: config) } @@ -19,6 +19,7 @@ public actor AIAssistantService { } public func chatWithCopilot(userMessage: String, history: [AIChatMessage], systemContext: String, + localSnapshot: LocalHeuristicProvider.SystemSnapshot? = nil, config: NIMConfig) async -> (reply: String, actions: [AIAction]) { let lower = userMessage.lowercased() var actions: [AIAction] = [] @@ -28,7 +29,7 @@ public actor AIAssistantService { if lower.contains("güncelle") || lower.contains("update") || lower.contains("yeni sürüm") || lower.contains("new version") { actions.append(AIAction(title: L10n.string("Check for Updates", table: .ai), type: .checkUpdates)) } if lower.contains("dns") || lower.contains("ağ") || lower.contains("internet") || lower.contains("network") { actions.append(AIAction(title: L10n.string("Reset DNS Cache", table: .ai), type: .flushDNS)) } do { - let reply = try await provider(for: config).queryCopilot(messages: Array(history.suffix(6)), snapshotContext: systemContext) + let reply = try await provider(for: config, localSnapshot: localSnapshot).queryCopilot(messages: Array(history.suffix(6)), snapshotContext: systemContext) return (reply, actions) } catch { return (L10n.string("NVIDIA NIM connection error. Local suggestions are offered in the action buttons below.", table: .ai), actions) diff --git a/Sources/MacOptimizer/Services/AutonomousGuardService.swift b/Sources/MacOptimizer/Services/AutonomousGuardService.swift index 70a9fe0..ec5e6e1 100644 --- a/Sources/MacOptimizer/Services/AutonomousGuardService.swift +++ b/Sources/MacOptimizer/Services/AutonomousGuardService.swift @@ -108,7 +108,7 @@ public actor AutonomousGuardService { lastThermalAlertTime = now let alert = AutonomousAlert( title: L10n.string("Thermal Throttling / Temperature Warning", table: .services), - message: L10n.string("CPU temperature has reached a critical threshold (%@). Close apps with high CPU usage to protect the hardware.", table: .services, cpu.thermalState.rawValue), + message: L10n.string("CPU temperature has reached a critical threshold (%@). Close apps with high CPU usage to protect the hardware.", table: .services, cpu.thermalState.localizedTitle), type: .runawayProcess, timestamp: now, isResolved: false, diff --git a/Sources/MacOptimizer/Views/AppManager/AppManagerView.swift b/Sources/MacOptimizer/Views/AppManager/AppManagerView.swift index 5879833..10cccf9 100644 --- a/Sources/MacOptimizer/Views/AppManager/AppManagerView.swift +++ b/Sources/MacOptimizer/Views/AppManager/AppManagerView.swift @@ -10,21 +10,23 @@ public struct AppManagerView: View { @State private var appToUninstall: InstalledApp? enum AppFilter: String, CaseIterable, Identifiable { - case all = "Tümü" - case userOnly = "Kullanıcı" + case all = "All" + case userOnly = "User" case appleSilicon = "Apple Silicon" case universal = "Universal" case intel = "Intel (x86)" - case updates = "Güncellemeler" + case updates = "Updates" var id: String { rawValue } + var label: String { L10n.string(rawValue, table: .cleanup) } } enum AppSortOption: String, CaseIterable, Identifiable { - case size = "Boyuta Göre" - case name = "İsme Göre" + case size = "By Size" + case name = "By Name" var id: String { rawValue } + var label: String { L10n.string(rawValue, table: .cleanup) } } private var filteredApps: [InstalledApp] { @@ -110,17 +112,17 @@ public struct AppManagerView: View { } VStack(alignment: .leading, spacing: 3) { - Text("Yüklü Uygulamalar & Kaldırıcı") + Text(l10n: "Installed Apps & Uninstaller", table: .cleanup) .font(.system(size: 16, weight: .bold)) .lineLimit(1) if !appState.installedApps.isEmpty { - Text("\(appState.installedApps.count) uygulama tespit edildi • Toplam: \(ByteFormatter.format(totalAppsSize))") + Text(L10n.string("%lld apps detected • Total: %@", table: .cleanup, appState.installedApps.count, ByteFormatter.format(totalAppsSize))) .font(.system(size: 12)) .foregroundColor(.secondary) .lineLimit(1) } else { - Text("Mac'inizde kurulu tüm uygulamaları, mimarilerini ve boyutlarını inceleyin.") + Text(l10n: "Review all apps installed on your Mac, their architectures, and sizes.", table: .cleanup) .font(.system(size: 12)) .foregroundColor(.secondary) .lineLimit(1) @@ -143,7 +145,7 @@ public struct AppManagerView: View { HStack(spacing: 8) { ActionButton( - title: appState.isScanningApps ? "Taranıyor..." : "Uygulamaları Tara", + title: appState.isScanningApps ? L10n.string("Scanning...", table: .cleanup) : L10n.string("Scan Apps", table: .cleanup), iconName: "arrow.clockwise", gradient: SystemTheme.primaryGradient, isLoading: appState.isScanningApps @@ -152,7 +154,7 @@ public struct AppManagerView: View { } ActionButton( - title: appState.isCheckingUpdates ? "Denetleniyor..." : "Güncellemeleri Bul", + title: appState.isCheckingUpdates ? L10n.string("Checking...", table: .cleanup) : L10n.string("Find Updates", table: .cleanup), iconName: "arrow.triangle.2.circlepath", gradient: SystemTheme.updateGradient, isLoading: appState.isCheckingUpdates @@ -202,7 +204,7 @@ public struct AppManagerView: View { Image(systemName: "magnifyingglass") .foregroundColor(.secondary) .font(.system(size: 12)) - TextField("Uygulama adı veya Bundle ID ara...", text: $searchText) + TextField(L10n.string("Search by app name or Bundle ID...", table: .cleanup), text: $searchText) .textFieldStyle(.plain) .font(.system(size: 12)) @@ -224,18 +226,18 @@ public struct AppManagerView: View { } private var filterSegmentedPicker: some View { - Picker("Filtrele", selection: $selectedFilter) { + Picker(L10n.string("Filter", table: .cleanup), selection: $selectedFilter) { ForEach(AppFilter.allCases) { filter in - Text(filter.rawValue).tag(filter) + Text(verbatim: filter.label).tag(filter) } } .pickerStyle(.segmented) } private var sortPicker: some View { - Picker("Sırala", selection: $sortOption) { + Picker(L10n.string("Sort", table: .cleanup), selection: $sortOption) { ForEach(AppSortOption.allCases) { opt in - Text(opt.rawValue).tag(opt) + Text(verbatim: opt.label).tag(opt) } } .pickerStyle(.menu) @@ -275,17 +277,17 @@ public struct AppManagerView: View { .font(.system(size: 48)) .foregroundStyle(SystemTheme.primaryGradient) - Text("Uygulamalar Taranmadı") + Text(l10n: "Apps Not Scanned", table: .cleanup) .font(.system(size: 16, weight: .bold)) - Text("Kurulu olan uygulamaları listelemek ve güncelleme kontrolü yapmak için taramayı başlatın.") + Text(l10n: "Start a scan to list installed apps and check for updates.", table: .cleanup) .font(.system(size: 13)) .foregroundColor(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 400) ActionButton( - title: "Uygulamaları Tara", + title: L10n.string("Scan Apps", table: .cleanup), iconName: "magnifyingglass", gradient: SystemTheme.primaryGradient ) { diff --git a/Sources/MacOptimizer/Views/AppManager/AppRowView.swift b/Sources/MacOptimizer/Views/AppManager/AppRowView.swift index 53ff2a5..6c5df27 100644 --- a/Sources/MacOptimizer/Views/AppManager/AppRowView.swift +++ b/Sources/MacOptimizer/Views/AppManager/AppRowView.swift @@ -23,11 +23,11 @@ public struct AppRowView: View { MetricBadge(text: app.architecture.shortName, colorName: app.architecture.badgeColor) if app.isSystemApp { - MetricBadge(text: "Sistem", colorName: "gray") + MetricBadge(text: L10n.string("System", table: .cleanup), colorName: "gray") } if app.updateInfo.hasUpdate { - MetricBadge(text: "Güncelleme: v\(app.updateInfo.latestVersion)", colorName: "orange") + MetricBadge(text: L10n.string("Update: v%@", table: .cleanup, app.updateInfo.latestVersion), colorName: "orange") } } @@ -61,7 +61,7 @@ public struct AppRowView: View { Button(action: onUpdate) { HStack(spacing: 4) { Image(systemName: "arrow.up.circle.fill") - Text("Güncelle") + Text(l10n: "Update", table: .cleanup) } .font(.system(size: 11, weight: .semibold)) .foregroundColor(.white) @@ -81,13 +81,13 @@ public struct AppRowView: View { .foregroundColor(.secondary) } .buttonStyle(.plain) - .help("Finder'da Göster") + .help(L10n.string("Show in Finder", table: .cleanup)) if !app.isSystemApp { Button(action: onInspectUninstall) { HStack(spacing: 4) { Image(systemName: "trash") - Text("Kaldır") + Text(l10n: "Uninstall", table: .cleanup) } .font(.system(size: 11, weight: .semibold)) .foregroundColor(.red) @@ -97,7 +97,7 @@ public struct AppRowView: View { .clipShape(RoundedRectangle(cornerRadius: 6)) } .buttonStyle(.plain) - .help("Kalıntılarıyla Birlikte Kaldır") + .help(L10n.string("Uninstall Along with Leftovers", table: .cleanup)) } } .frame(minWidth: 90, alignment: .trailing) diff --git a/Sources/MacOptimizer/Views/AppManager/AppUninstallerDetailView.swift b/Sources/MacOptimizer/Views/AppManager/AppUninstallerDetailView.swift index 379418c..b83971b 100644 --- a/Sources/MacOptimizer/Views/AppManager/AppUninstallerDetailView.swift +++ b/Sources/MacOptimizer/Views/AppManager/AppUninstallerDetailView.swift @@ -20,7 +20,7 @@ public struct AppUninstallerDetailView: View { .frame(width: 48, height: 48) VStack(alignment: .leading, spacing: 2) { - Text("\(app.name) Kaldırılıyor") + Text(L10n.string("Uninstalling %@", table: .cleanup, app.name)) .font(.system(size: 16, weight: .bold)) Text("v\(app.version) • \(app.bundleIdentifier)") @@ -30,7 +30,7 @@ public struct AppUninstallerDetailView: View { Spacer() - Button("Kapat") { + Button(L10n.string("Close", table: .cleanup)) { onDismiss() } .buttonStyle(.plain) @@ -44,7 +44,7 @@ public struct AppUninstallerDetailView: View { if appState.isLoadingAppFiles { VStack(spacing: 12) { ProgressView() - Text("İlişkili dosyalar ve önbellekler aranıyor...") + Text(l10n: "Searching for related files and caches...", table: .cleanup) .font(.system(size: 13)) .foregroundColor(.secondary) } @@ -52,12 +52,12 @@ public struct AppUninstallerDetailView: View { } else { VStack(alignment: .leading, spacing: 10) { HStack { - Text("Bulunan İlişkili Dosyalar (\(appState.selectedAppFiles.count) öğe)") + Text(L10n.string("Related Files Found (%lld items)", table: .cleanup, appState.selectedAppFiles.count)) .font(.system(size: 13, weight: .semibold)) Spacer() - Text("Kazanılacak Alan: \(ByteFormatter.format(totalSizeBytes))") + Text(L10n.string("Space to Reclaim: %@", table: .cleanup, ByteFormatter.format(totalSizeBytes))) .font(.system(size: 13, weight: .bold)) .foregroundColor(.red) } @@ -117,7 +117,7 @@ public struct AppUninstallerDetailView: View { // Actions HStack { - Button("Vazgeç") { + Button(L10n.string("Cancel", table: .cleanup)) { onDismiss() } .buttonStyle(.plain) @@ -126,7 +126,7 @@ public struct AppUninstallerDetailView: View { Spacer() ActionButton( - title: appState.isUninstalling ? "Kaldırılıyor..." : "Tümünü Sil ve Kaldır (\(ByteFormatter.format(totalSizeBytes)))", + title: appState.isUninstalling ? L10n.string("Uninstalling...", table: .cleanup) : L10n.string("Delete All and Uninstall (%@)", table: .cleanup, ByteFormatter.format(totalSizeBytes)), iconName: "trash.fill", gradient: SystemTheme.dangerGradient, isLoading: appState.isUninstalling @@ -141,13 +141,13 @@ public struct AppUninstallerDetailView: View { .clipShape(RoundedRectangle(cornerRadius: 16)) .alert(isPresented: $showConfirmAlert) { Alert( - title: Text("\(app.name) Uygulamasını Kaldır"), - message: Text("Uygulama paketi Çöp Sepetine taşınacak. Seçili kalıntılar da Çöp Sepetine taşınacaktır. Devam edilsin mi?"), - primaryButton: .destructive(Text("Kalıntılarıyla Kaldır")) { + title: Text(L10n.string("Uninstall %@", table: .cleanup, app.name)), + message: Text(l10n: "The app bundle will be moved to the Trash. Selected leftovers will also be moved to the Trash. Continue?", table: .cleanup), + primaryButton: .destructive(Text(l10n: "Uninstall with Leftovers", table: .cleanup)) { appState.performUninstall() onDismiss() }, - secondaryButton: .cancel(Text("Vazgeç")) + secondaryButton: .cancel(Text(l10n: "Cancel", table: .cleanup)) ) } } diff --git a/Sources/MacOptimizer/Views/AppManager/AppUpdatesView.swift b/Sources/MacOptimizer/Views/AppManager/AppUpdatesView.swift index c33614d..8708de3 100644 --- a/Sources/MacOptimizer/Views/AppManager/AppUpdatesView.swift +++ b/Sources/MacOptimizer/Views/AppManager/AppUpdatesView.swift @@ -43,17 +43,17 @@ public struct AppUpdatesView: View { } VStack(alignment: .leading, spacing: 3) { - Text("Yazılım & Uygulama Güncellemeleri") + Text(l10n: "Software & App Updates", table: .cleanup) .font(.system(size: 16, weight: .bold)) .lineLimit(1) if !appsWithUpdates.isEmpty { - Text("\(appsWithUpdates.count) uygulama için yeni sürüm mevcut!") + Text(L10n.string("New versions available for %lld apps!", table: .cleanup, appsWithUpdates.count)) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.orange) .lineLimit(1) } else { - Text("Yüklü macOS uygulamalarınızın en son sürümlerini kontrol edin.") + Text(l10n: "Check for the latest versions of your installed macOS apps.", table: .cleanup) .font(.system(size: 12)) .foregroundColor(.secondary) .lineLimit(1) @@ -75,7 +75,7 @@ public struct AppUpdatesView: View { Spacer(minLength: 12) ActionButton( - title: appState.isCheckingUpdates ? "Denetleniyor..." : "Güncellemeleri Denetle", + title: appState.isCheckingUpdates ? L10n.string("Checking...", table: .cleanup) : L10n.string("Check for Updates", table: .cleanup), iconName: "arrow.triangle.2.circlepath", gradient: SystemTheme.updateGradient, isLoading: appState.isCheckingUpdates @@ -108,7 +108,7 @@ public struct AppUpdatesView: View { } HStack(spacing: 6) { - Text("Mevcut: v\(app.version)") + Text(L10n.string("Current: v%@", table: .cleanup, app.version)) .font(.system(size: 11)) .foregroundColor(.secondary) @@ -116,7 +116,7 @@ public struct AppUpdatesView: View { .font(.system(size: 9)) .foregroundColor(.secondary) - Text("Yeni: v\(app.updateInfo.latestVersion)") + Text(L10n.string("New: v%@", table: .cleanup, app.updateInfo.latestVersion)) .font(.system(size: 11, weight: .bold)) .foregroundColor(.green) } @@ -126,7 +126,7 @@ public struct AppUpdatesView: View { HStack(spacing: 8) { if !app.updateInfo.releaseNotesURL.isEmpty, let url = URL(string: app.updateInfo.releaseNotesURL) { - Button("Sürüm Notları") { + Button(L10n.string("Release Notes", table: .cleanup)) { NSWorkspace.shared.open(url) } .buttonStyle(.plain) @@ -139,7 +139,7 @@ public struct AppUpdatesView: View { if app.updateInfo.updateSource == .homebrew { // Only Homebrew-sourced entries may run brew, and only with a validated token. if let token = AppUpdateCheckerService.caskToken(fromUpgradeCommand: app.updateInfo.downloadURL) { - appState.showNotification(message: "\(app.name) Homebrew üzerinden güncelleniyor.") + appState.showNotification(message: L10n.string("Updating %@ via Homebrew.", table: .cleanup, app.name)) Task { let result = await AppUpdateCheckerService.shared.upgradeHomebrewCask(token: token) appState.showNotification(message: result.message) @@ -152,7 +152,7 @@ public struct AppUpdatesView: View { } label: { HStack(spacing: 4) { Image(systemName: "arrow.down.circle.fill") - Text("Güncellemeyi Al") + Text(l10n: "Get Update", table: .cleanup) } .font(.system(size: 11, weight: .semibold)) .foregroundColor(.white) @@ -178,10 +178,10 @@ public struct AppUpdatesView: View { .font(.system(size: 48)) .foregroundStyle(SystemTheme.memoryGradient) - Text("Tüm Uygulamalarınız Güncel") + Text(l10n: "All Your Apps Are Up to Date", table: .cleanup) .font(.system(size: 16, weight: .bold)) - Text("Herhangi bir bekleyen yazılım güncellemesi bulunamadı. Yeni sürümleri denetlemek için yukarıdaki butonu kullanabilirsiniz.") + Text(l10n: "No pending software updates were found. Use the button above to check for new versions.", table: .cleanup) .font(.system(size: 13)) .foregroundColor(.secondary) .multilineTextAlignment(.center) diff --git a/Tests/MacOptimizerTests/MacOptimizerTests.swift b/Tests/MacOptimizerTests/MacOptimizerTests.swift index 5d70445..82d769d 100644 --- a/Tests/MacOptimizerTests/MacOptimizerTests.swift +++ b/Tests/MacOptimizerTests/MacOptimizerTests.swift @@ -581,8 +581,36 @@ final class MacOptimizerTests: XCTestCase { let turkish = await service.chatWithCopilot(userMessage: "Önbellek temizle ve DNS sıfırla", history: [], systemContext: "", config: NIMConfig()) XCTAssertEqual(turkish.actions.map(\.type), [.scanJunk, .flushDNS]) - let memory = try? await LocalHeuristicProvider().queryCopilot(messages: [AIChatMessage(role: .user, content: "Why is memory high?")], snapshotContext: "ctx") - XCTAssertTrue(memory?.hasSuffix("ctx") == true) + let question = [AIChatMessage(role: .user, content: "Why is memory high?")] + let withoutSnapshot = try? await LocalHeuristicProvider().queryCopilot(messages: question, snapshotContext: "ctx") + XCTAssertEqual(withoutSnapshot, L10n.string("I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.", table: .ai)) + + var memoryStats = MemoryStats() + memoryStats.totalBytes = 16_000_000_000 + memoryStats.activeBytes = 8_000_000_000 + let snapshot = LocalHeuristicProvider.SystemSnapshot(hardware: HardwareInfo(), memory: memoryStats, cpu: CPUStats(), disk: DiskStats()) + let memory = try? await LocalHeuristicProvider(snapshot: snapshot).queryCopilot(messages: question, snapshotContext: "ctx") + XCTAssertFalse(memory?.contains("ctx") ?? true, "The English cloud snapshot must not be shown to the user") + XCTAssertTrue(memory?.hasSuffix(LocalHeuristicProvider.localizedSummary(snapshot)) == true) + } + + func testOfflineMemoryReplyIsLocalizedForTurkishUsers() async { + let prior = UserDefaults.standard.object(forKey: "AppleLanguages") + defer { + if let prior { UserDefaults.standard.set(prior, forKey: "AppleLanguages") } + else { UserDefaults.standard.removeObject(forKey: "AppleLanguages") } + } + UserDefaults.standard.set(["tr"], forKey: "AppleLanguages") + var memoryStats = MemoryStats() + memoryStats.totalBytes = 16_000_000_000 + memoryStats.activeBytes = 8_000_000_000 + let snapshot = LocalHeuristicProvider.SystemSnapshot(hardware: HardwareInfo(), memory: memoryStats, cpu: CPUStats(), disk: DiskStats()) + let reply = try? await LocalHeuristicProvider(snapshot: snapshot) + .queryCopilot(messages: [AIChatMessage(role: .user, content: "bellek neden dolu?")], snapshotContext: "Mac model: X") + XCTAssertTrue(reply?.contains("Mac modeli:") == true, reply ?? "") + XCTAssertTrue(reply?.contains("RAM: %50") == true, reply ?? "") + XCTAssertFalse(reply?.contains("Mac model:") ?? true, reply ?? "") + XCTAssertFalse(reply?.contains("Free disk") ?? true, reply ?? "") } // MARK: - 11. Mach-O Architecture Detector Tests @@ -784,6 +812,34 @@ final class MacOptimizerTests: XCTestCase { XCTAssertTrue(CLICommandRunner.shouldHandleCLI(arguments: ["MacOptimizer", "unknown"])) } + func testCLIStatusOutputIsEnglishOnly() { + let prior = UserDefaults.standard.object(forKey: "AppleLanguages") + defer { + if let prior { UserDefaults.standard.set(prior, forKey: "AppleLanguages") } + else { UserDefaults.standard.removeObject(forKey: "AppleLanguages") } + } + UserDefaults.standard.set(["tr"], forKey: "AppleLanguages") + let turkishCharacters = CharacterSet(charactersIn: "çğıİöşüÇĞÖŞÜ") + var battery = BatteryStats() + battery.powerSource = "AC Adapter" + var hardware = HardwareInfo() + hardware.uptimeString = "2 d, 3 h" + for level in [MemoryStats.MemoryPressureLevel.normal, .warning, .critical] { + for thermal in CPUStats.ThermalState.allCases { + var memory = MemoryStats() + memory.pressureLevel = level + var cpu = CPUStats() + cpu.thermalState = thermal + let report = CLICommandRunner.statusReport(memory: memory, cpu: cpu, disk: DiskStats(), battery: battery, hardware: hardware) + XCTAssertNil(report.rangeOfCharacter(from: turkishCharacters), report) + XCTAssertFalse(report.contains(level.rawValue) && level != .normal, report) + XCTAssertFalse(report.contains(thermal.rawValue), report) + XCTAssertTrue(report.contains("Memory Pressure: \(CLICommandRunner.englishLabel(for: level))")) + XCTAssertTrue(report.contains("Thermal State: \(CLICommandRunner.englishLabel(for: thermal))")) + } + } + } + func testCLIHelpIsEnglishOnly() { XCTAssertFalse(CLICommandRunner.helpText.contains { "çğıİöşüÇĞÖŞÜ".contains($0) }) }