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) })
}