From b9fc00f99db72c11f181b75dd86e13c150e0818d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Osman=20=C3=87a=C4=9Fr=C4=B1=20GEN=C3=87?= Date: Tue, 29 Sep 2026 01:02:27 +0300 Subject: [PATCH] feat(l10n): localize AI services, English system prompt Co-Authored-By: Claude Opus 5.5 --- .../AI/LocalHeuristicProvider.swift | 62 ++++++++++--------- .../Infrastructure/AI/NvidiaNIMProvider.swift | 20 ++++-- Sources/MacOptimizer/L10n.swift | 15 ++++- .../Resources/en.lproj/ai.strings | 47 ++++++++++++++ .../Resources/en.lproj/ai.stringsdict | 22 +++++++ .../Resources/tr.lproj/ai.strings | 47 ++++++++++++++ .../Resources/tr.lproj/ai.stringsdict | 22 +++++++ .../Services/AIAssistantService.swift | 10 +-- .../Services/NvidiaNIMService.swift | 44 ++++++------- .../MacOptimizerTests/MacOptimizerTests.swift | 35 ++++++++++- 10 files changed, 259 insertions(+), 65 deletions(-) create mode 100644 Sources/MacOptimizer/Resources/en.lproj/ai.stringsdict create mode 100644 Sources/MacOptimizer/Resources/tr.lproj/ai.stringsdict diff --git a/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift b/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift index ed5bde5..21c2111 100644 --- a/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift +++ b/Sources/MacOptimizer/Infrastructure/AI/LocalHeuristicProvider.swift @@ -3,7 +3,7 @@ import Foundation /// 100% Offline, zero-network rule-based diagnostic and intelligence provider. public struct LocalHeuristicProvider: AIProvider { public let providerId: String = "local_heuristics" - public let displayName: String = "Yerel Kural Motoru (Çevrimdışı & Güvenli)" + public let displayName: String = L10n.string("Local Rule Engine (100% Offline & Secure)", table: .ai) public let requiresNetwork: Bool = false public init() {} @@ -20,22 +20,22 @@ public struct LocalHeuristicProvider: AIProvider { var insights: [AIInsight] = [] let heaviestApps = topProcesses.filter { $0.isUserApp && !$0.isProtected }.prefix(3) let memoryAdvice = heaviestApps.isEmpty - ? "En çok bellek kullanan uygulamaları kapatmayı deneyin." - : "Bellek kullanımını azaltmak için şu yoğun uygulamaları kapatmayı deneyin: " + heaviestApps.map { "\($0.name) (\($0.memoryFormatted))" }.joined(separator: ", ") + "." + ? L10n.string("Try closing the apps using the most memory.", table: .ai) + : L10n.string("To reduce memory usage, try closing these heavy apps: %@.", table: .ai, heaviestApps.map { "\($0.name) (\($0.memoryFormatted))" }.joined(separator: ", ")) // 1. Memory Pressure Evaluation if memory.pressureLevel == .critical || memory.usedPercentage > 0.88 { insights.append(AIInsight( - title: "Kritik Bellek Baskısı Tespit Edildi", - summary: "RAM kullanımı %\(Int(memory.usedPercentage * 100)) seviyesinde. \(memoryAdvice)", + title: L10n.string("Critical Memory Pressure Detected", table: .ai), + summary: L10n.string("RAM usage is at %lld%%. %@", table: .ai, Int(memory.usedPercentage * 100), memoryAdvice), severity: .critical, category: "RAM", actions: [] )) } else if memory.pressureLevel == .warning || memory.usedPercentage > 0.75 { insights.append(AIInsight( - title: "Orta Düzey Bellek Yoğunluğu", - summary: "Bellek doluluğu %\(Int(memory.usedPercentage * 100)). \(memoryAdvice)", + title: L10n.string("Moderate Memory Load", table: .ai), + summary: L10n.string("Memory usage is %lld%%. %@", table: .ai, Int(memory.usedPercentage * 100), memoryAdvice), severity: .warning, category: "RAM", actions: [] @@ -46,12 +46,12 @@ public struct LocalHeuristicProvider: AIProvider { let heavyProcs = topProcesses.filter { $0.cpuPercentage > 75.0 && !$0.isProtected } if let runaway = heavyProcs.first { insights.append(AIInsight( - title: "Yüksek CPU Tüketen Süreç: \(runaway.name)", - summary: "\(runaway.name) (PID: \(runaway.pid)) tek başına %\(String(format: "%.1f", runaway.cpuPercentage)) CPU tüketiyor. Bu durum fan gürültüsüne ve pilin hızlı tükenmesine yol açabilir.", + title: L10n.string("High CPU Process: %@", table: .ai, runaway.name), + summary: L10n.string("%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain.", table: .ai, runaway.name, runaway.pid, String(format: "%.1f", runaway.cpuPercentage)), severity: .warning, - category: "İşlemci", + category: L10n.string("CPU", table: .ai), actions: [ - AIAction(title: "\(runaway.name) Kapat", type: .killProcess, targetPID: runaway.pid) + AIAction(title: L10n.string("Quit %@", table: .ai, runaway.name), type: .killProcess, targetPID: runaway.pid) ] )) } @@ -60,12 +60,12 @@ public struct LocalHeuristicProvider: AIProvider { let totalJunk = junkGroups.reduce(0) { $0 + $1.totalSizeBytes } if totalJunk > 3 * 1024 * 1024 * 1024 { insights.append(AIInsight( - title: "\(ByteFormatter.format(totalJunk)) Gereksiz Önbellek Birikimi", - summary: "Xcode DerivedData, sistem logları ve tarayıcı önbelleklerinde önemli miktarda yer kazanılabilir alan tespit edildi.", + title: L10n.string("%@ of Unneeded Cache Buildup", table: .ai, ByteFormatter.format(totalJunk)), + summary: L10n.string("A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches.", table: .ai), severity: .recommendation, - category: "Disk", + category: L10n.string("Disk", table: .ai), actions: [ - AIAction(title: "Gereksiz Dosyaları Temizle", type: .cleanJunk) + AIAction(title: L10n.string("Clean Junk Files", table: .ai), type: .cleanJunk) ] )) } @@ -73,12 +73,12 @@ public struct LocalHeuristicProvider: AIProvider { // 4. Software Updates if outdatedAppsCount > 0 { insights.append(AIInsight( - title: "\(outdatedAppsCount) Uygulama Güncellemesi Mevcut", - summary: "Yüklü uygulamalarınızın yeni sürümleri yayınlandı. En son güvenlik yamaları ve performans iyileştirmeleri için güncellemeniz önerilir.", + title: L10n.string("%lld App Updates Available", table: .ai, outdatedAppsCount), + summary: L10n.string("New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements.", table: .ai), severity: .info, - category: "Yazılım", + category: L10n.string("Software", table: .ai), actions: [ - AIAction(title: "Güncellemeleri İncele", type: .checkUpdates) + AIAction(title: L10n.string("Review Updates", table: .ai), type: .checkUpdates) ] )) } @@ -86,10 +86,10 @@ public struct LocalHeuristicProvider: AIProvider { // 5. Default Healthy State if insights.isEmpty { insights.append(AIInsight( - title: "Mac'iniz Harika Durumda", - summary: "Bellek baskısı normal seviyede, olağandışı kaçak CPU süreci bulunmuyor ve disk alanınız dengeli.", + title: L10n.string("Your Mac Is in Great Shape", table: .ai), + summary: L10n.string("Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced.", table: .ai), severity: .info, - category: "Genel" + category: L10n.string("General", table: .ai) )) } @@ -101,17 +101,19 @@ public struct LocalHeuristicProvider: AIProvider { snapshotContext: String ) async throws -> String { guard let lastUserMsg = messages.last(where: { $0.role == .user })?.content.lowercased() else { - return "Size nasıl yardımcı olabilirim?" + return L10n.string("How can I help you?", table: .ai) } - if lastUserMsg.contains("ram") || lastUserMsg.contains("bellek") { - return "Mac'inizin bellek durumunu inceledim. Bellek baskısını azaltmak için en yoğun uygulamaları kapatmayı deneyebilirsiniz.\n\nSistem Bilgisi:\n\(snapshotContext)" - } else if lastUserMsg.contains("ısın") || lastUserMsg.contains("cpu") || lastUserMsg.contains("fan") { - return "İşlemci ve donanım telemetrisine göre en çok kaynak tüketen süreçleri Görev Yöneticisi'nden kontrol edebilir, askıda kalan kullanıcı uygulamalarını güvenle sonlandırabilirsiniz." - } else if lastUserMsg.contains("temiz") || lastUserMsg.contains("disk") || lastUserMsg.contains("yer") { - return "Disk Temizleyici modülü ile sistem önbelleklerini, derleme kalıntılarını (Xcode DerivedData) ve tarayıcı verilerini güvenle temizleyebilirsiniz." + // 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) + } 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") + || lastUserMsg.contains("clean") || lastUserMsg.contains("space") || lastUserMsg.contains("storage") { + return L10n.string("With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data.", table: .ai) } else { - return "MacOptimizer Pro yerel yapay zeka motoru devrede. Mac'inizin performansı, bellek yönetimi ve sistem sağlığı hakkında dilediğiniz soruyu sorabilirsiniz." + return L10n.string("The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health.", table: .ai) } } } diff --git a/Sources/MacOptimizer/Infrastructure/AI/NvidiaNIMProvider.swift b/Sources/MacOptimizer/Infrastructure/AI/NvidiaNIMProvider.swift index 16f5c10..bb7d960 100644 --- a/Sources/MacOptimizer/Infrastructure/AI/NvidiaNIMProvider.swift +++ b/Sources/MacOptimizer/Infrastructure/AI/NvidiaNIMProvider.swift @@ -2,7 +2,7 @@ import Foundation public struct NvidiaNIMProvider: AIProvider { public let providerId = "nvidia_nim" - public let displayName = "NVIDIA NIM (Bulut)" + public let displayName = L10n.string("NVIDIA NIM (Cloud)", table: .ai) public let requiresNetwork = true private let config: NIMConfig @@ -10,17 +10,27 @@ public struct NvidiaNIMProvider: AIProvider { public nonisolated static func cloudContext(_ base: String, processNames: [String], includeProcesses: Bool) -> String { guard includeProcesses, !processNames.isEmpty else { return base } - return base + "\nÇalışan uygulamalar: " + processNames.joined(separator: ", ") + return base + "\nRunning apps: " + processNames.joined(separator: ", ") + } + + /// Model instructions are always English; the reply language follows the app's UI language. + public nonisolated static func diagnosisPrompt(_ metrics: String, replyLanguage: String) -> String { + metrics + " Give short system recommendations. Reply in \(replyLanguage)." + } + + public nonisolated static func copilotSystemPrompt(snapshotContext: String, replyLanguage: String) -> String { + "You are the MacOptimizer Pro macOS assistant. Reply in \(replyLanguage); only offer recommendations, never execute actions. System summary: \(snapshotContext)" } public func diagnose(memory: MemoryStats, cpu: CPUStats, disk: DiskStats, hardware: HardwareInfo, topProcesses: [ProcessInfoModel], junkGroups: [JunkCategoryGroup], outdatedAppsCount: Int) async -> [AIInsight] { let totalJunk = junkGroups.reduce(0) { $0 + $1.totalSizeBytes } - let base = "Mac: \(hardware.modelName), \(hardware.chipName), \(hardware.osVersion). RAM %\(Int(memory.usedPercentage * 100)), CPU %\(cpu.totalUsage), Boş disk %\(Int(disk.freePercentage * 100)), Gereksiz dosyalar \(ByteFormatter.format(totalJunk)), Güncelleme bekleyen uygulama: \(outdatedAppsCount). Türkçe kısa sistem önerileri ver." + let metrics = "Mac: \(hardware.modelName), \(hardware.chipName), \(hardware.osVersion). RAM \(Int(memory.usedPercentage * 100))%, CPU \(cpu.totalUsage)%, Free disk \(Int(disk.freePercentage * 100))%, Junk files \(ByteFormatter.format(totalJunk)), Apps with pending updates: \(outdatedAppsCount)." + let base = Self.diagnosisPrompt(metrics, replyLanguage: L10n.currentLanguageEnglishName()) let prompt = Self.cloudContext(base, processNames: topProcesses.prefix(4).map { "\($0.name) (PID: \($0.pid))" }, includeProcesses: UserDefaults.standard.bool(forKey: "MacOptimizer_IncludeRunningAppNames")) do { let answer = try await NvidiaNIMService.shared.sendChatCompletion(messages: [["role": "user", "content": prompt]], config: config) - return [AIInsight(title: "NVIDIA NIM Sistem Analizi", summary: answer, severity: .recommendation, category: "Yapay Zeka Raporu")] + return [AIInsight(title: L10n.string("NVIDIA NIM System Analysis", table: .ai), summary: answer, severity: .recommendation, category: L10n.string("AI Report", table: .ai))] } catch { return await LocalHeuristicProvider().diagnose(memory: memory, cpu: cpu, disk: disk, hardware: hardware, topProcesses: topProcesses, junkGroups: junkGroups, outdatedAppsCount: outdatedAppsCount) @@ -28,7 +38,7 @@ public struct NvidiaNIMProvider: AIProvider { } public func queryCopilot(messages: [AIChatMessage], snapshotContext: String) async throws -> String { - let system = "MacOptimizer Pro macOS asistanısın. Türkçe yanıt ver; sadece öneri sun, eylem çalıştırma. Sistem özeti: \(snapshotContext)" + let system = Self.copilotSystemPrompt(snapshotContext: snapshotContext, replyLanguage: L10n.currentLanguageEnglishName()) let chat = messages.suffix(6).map { ["role": $0.role.rawValue, "content": $0.content] } return try await NvidiaNIMService.shared.sendChatCompletion(messages: chat, config: config, systemPrompt: system) } diff --git a/Sources/MacOptimizer/L10n.swift b/Sources/MacOptimizer/L10n.swift index 69f78aa..1051043 100644 --- a/Sources/MacOptimizer/L10n.swift +++ b/Sources/MacOptimizer/L10n.swift @@ -6,15 +6,26 @@ public enum L10n { case common, dashboard, cleanup, ai, services } - public static func string(_ key: String, table: Table = .common) -> String { + /// The module localization (e.g. "en", "tr") that `string(_:table:)` resolves against, if any. + public static func currentLanguage() -> String? { let preferences = UserDefaults.standard.stringArray(forKey: "AppleLanguages") ?? Bundle.main.preferredLocalizations - let language = preferences.lazy.compactMap { preference in + return preferences.lazy.compactMap { preference in Bundle.module.localizations.first { preference.caseInsensitiveCompare($0) == .orderedSame || preference.lowercased().hasPrefix($0.lowercased() + "-") } }.first + } + + /// English name of the UI language in use (e.g. "Turkish", "English"), for model instructions. + public static func currentLanguageEnglishName() -> String { + let code = currentLanguage() ?? Bundle.module.developmentLocalization ?? "en" + return Locale(identifier: "en").localizedString(forLanguageCode: code) ?? "English" + } + + public static func string(_ key: String, table: Table = .common) -> String { + let language = currentLanguage() if let language, let path = Bundle.module.path(forResource: language, ofType: "lproj"), let bundle = Bundle(path: path) { diff --git a/Sources/MacOptimizer/Resources/en.lproj/ai.strings b/Sources/MacOptimizer/Resources/en.lproj/ai.strings index 7726883..6d288dd 100644 --- a/Sources/MacOptimizer/Resources/en.lproj/ai.strings +++ b/Sources/MacOptimizer/Resources/en.lproj/ai.strings @@ -61,3 +61,50 @@ "General & Menu Bar" = "General & Menu Bar"; "Show live status icon in the menu bar" = "Show live status icon in the menu bar"; "Open-source, security-first, local macOS system health and optimization toolkit." = "Open-source, security-first, local macOS system health and optimization toolkit."; +"NVIDIA NIM (Cloud)" = "NVIDIA NIM (Cloud)"; +"Try closing the apps using the most memory." = "Try closing the apps using the most memory."; +"To reduce memory usage, try closing these heavy apps: %@." = "To reduce memory usage, try closing these heavy apps: %@."; +"Critical Memory Pressure Detected" = "Critical Memory Pressure Detected"; +"RAM usage is at %lld%%. %@" = "RAM usage is at %lld%%. %@"; +"Moderate Memory Load" = "Moderate Memory Load"; +"Memory usage is %lld%%. %@" = "Memory usage is %lld%%. %@"; +"High CPU Process: %@" = "High CPU Process: %@"; +"%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain." = "%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain."; +"CPU" = "CPU"; +"Quit %@" = "Quit %@"; +"%@ of Unneeded Cache Buildup" = "%@ of Unneeded Cache Buildup"; +"A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches." = "A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches."; +"Disk" = "Disk"; +"Clean Junk Files" = "Clean Junk Files"; +"New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements." = "New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements."; +"Software" = "Software"; +"Review Updates" = "Review Updates"; +"Your Mac Is in Great Shape" = "Your Mac Is in Great Shape"; +"Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced." = "Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced."; +"General" = "General"; +"How can I help you?" = "How can I help you?"; +"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@" = "I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@"; +"Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps." = "Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps."; +"With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data." = "With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data."; +"The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health." = "The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health."; +"NVIDIA NIM System Analysis" = "NVIDIA NIM System Analysis"; +"AI Report" = "AI Report"; +"Check for Updates" = "Check for Updates"; +"Reset DNS Cache" = "Reset DNS Cache"; +"NVIDIA NIM connection error. Local suggestions are offered in the action buttons below." = "NVIDIA NIM connection error. Local suggestions are offered in the action buttons below."; +"Invalid Base URL format." = "Invalid Base URL format."; +"Security Warning: HTTPS is required for remote API connections." = "Security Warning: HTTPS is required for remote API connections."; +"Unsupported URL protocol. (Use https://)" = "Unsupported URL protocol. (Use https://)"; +"NVIDIA NIM API key is missing. Please enter your key." = "NVIDIA NIM API key is missing. Please enter your key."; +"Invalid /models URL." = "Invalid /models URL."; +"Received an invalid HTTP response from the server." = "Received an invalid HTTP response from the server."; +"NVIDIA NIM Error (%lld): %@" = "NVIDIA NIM Error (%lld): %@"; +"HTTP Error Code: %lld" = "HTTP Error Code: %lld"; +"Could not parse the model list." = "Could not parse the model list."; +"Model scanned via NVIDIA NIM." = "Model scanned via NVIDIA NIM."; +"No NVIDIA NIM API key entered. Please enter your key starting with 'nvapi-...'." = "No NVIDIA NIM API key entered. Please enter your key starting with 'nvapi-...'."; +"NVIDIA NIM connection and model ('%@') verified!" = "NVIDIA NIM connection and model ('%@') verified!"; +"Connection error: %@" = "Connection error: %@"; +"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."; diff --git a/Sources/MacOptimizer/Resources/en.lproj/ai.stringsdict b/Sources/MacOptimizer/Resources/en.lproj/ai.stringsdict new file mode 100644 index 0000000..f82321c --- /dev/null +++ b/Sources/MacOptimizer/Resources/en.lproj/ai.stringsdict @@ -0,0 +1,22 @@ + + + + %lld App Updates Available + NSStringLocalizedFormatKey%#@count@ + count + NSStringFormatSpecTypeKeyNSStringPluralRuleType + NSStringFormatValueTypeKeylld + one%lld App Update Available + other%lld App Updates Available + + + NVIDIA NIM connection successful! %lld models scanned. + NSStringLocalizedFormatKey%#@count@ + count + NSStringFormatSpecTypeKeyNSStringPluralRuleType + NSStringFormatValueTypeKeylld + oneNVIDIA NIM connection successful! %lld model scanned. + otherNVIDIA NIM connection successful! %lld models scanned. + + + diff --git a/Sources/MacOptimizer/Resources/tr.lproj/ai.strings b/Sources/MacOptimizer/Resources/tr.lproj/ai.strings index 090b11c..c76a9db 100644 --- a/Sources/MacOptimizer/Resources/tr.lproj/ai.strings +++ b/Sources/MacOptimizer/Resources/tr.lproj/ai.strings @@ -61,3 +61,50 @@ "General & Menu Bar" = "Genel ve Menü Çubuğu"; "Show live status icon in the menu bar" = "Menü Çubuğunda (Menu Bar) canlı durum simgesi göster"; "Open-source, security-first, local macOS system health and optimization toolkit." = "Açık kaynaklı, güvenlik öncelikli, yerel macOS sistem sağlığı ve optimizasyon araç seti."; +"NVIDIA NIM (Cloud)" = "NVIDIA NIM (Bulut)"; +"Try closing the apps using the most memory." = "En çok bellek kullanan uygulamaları kapatmayı deneyin."; +"To reduce memory usage, try closing these heavy apps: %@." = "Bellek kullanımını azaltmak için şu yoğun uygulamaları kapatmayı deneyin: %@."; +"Critical Memory Pressure Detected" = "Kritik Bellek Baskısı Tespit Edildi"; +"RAM usage is at %lld%%. %@" = "RAM kullanımı %%%lld seviyesinde. %@"; +"Moderate Memory Load" = "Orta Düzey Bellek Yoğunluğu"; +"Memory usage is %lld%%. %@" = "Bellek doluluğu %%%lld. %@"; +"High CPU Process: %@" = "Yüksek CPU Tüketen Süreç: %@"; +"%@ (PID: %d) alone is using CPU at %@%%. This can cause fan noise and fast battery drain." = "%@ (PID: %d) tek başına %%%@ CPU tüketiyor. Bu durum fan gürültüsüne ve pilin hızlı tükenmesine yol açabilir."; +"CPU" = "İşlemci"; +"Quit %@" = "%@ Kapat"; +"%@ of Unneeded Cache Buildup" = "%@ Gereksiz Önbellek Birikimi"; +"A significant amount of reclaimable space was found in Xcode DerivedData, system logs, and browser caches." = "Xcode DerivedData, sistem logları ve tarayıcı önbelleklerinde önemli miktarda yer kazanılabilir alan tespit edildi."; +"Disk" = "Disk"; +"Clean Junk Files" = "Gereksiz Dosyaları Temizle"; +"New versions of your installed apps have been released. Updating is recommended for the latest security patches and performance improvements." = "Yüklü uygulamalarınızın yeni sürümleri yayınlandı. En son güvenlik yamaları ve performans iyileştirmeleri için güncellemeniz önerilir."; +"Software" = "Yazılım"; +"Review Updates" = "Güncellemeleri İncele"; +"Your Mac Is in Great Shape" = "Mac'iniz Harika Durumda"; +"Memory pressure is normal, there is no unusual runaway CPU process, and your disk space is balanced." = "Bellek baskısı normal seviyede, olağandışı kaçak CPU süreci bulunmuyor ve disk alanınız dengeli."; +"General" = "Genel"; +"How can I help you?" = "Size nasıl yardımcı olabilirim?"; +"I reviewed your Mac’s memory status. To reduce memory pressure, you can try closing the most demanding apps.\n\nSystem Info:\n%@" = "Mac'inizin bellek durumunu inceledim. Bellek baskısını azaltmak için en yoğun uygulamaları kapatmayı deneyebilirsiniz.\n\nSistem Bilgisi:\n%@"; +"Based on processor and hardware telemetry, you can check the most resource-hungry processes in Task Manager and safely terminate unresponsive user apps." = "İşlemci ve donanım telemetrisine göre en çok kaynak tüketen süreçleri Görev Yöneticisi'nden kontrol edebilir, askıda kalan kullanıcı uygulamalarını güvenle sonlandırabilirsiniz."; +"With the Disk Cleaner module, you can safely clean system caches, build leftovers (Xcode DerivedData), and browser data." = "Disk Temizleyici modülü ile sistem önbelleklerini, derleme kalıntılarını (Xcode DerivedData) ve tarayıcı verilerini güvenle temizleyebilirsiniz."; +"The MacOptimizer Pro local AI engine is active. Ask any question about your Mac’s performance, memory management, and system health." = "MacOptimizer Pro yerel yapay zeka motoru devrede. Mac'inizin performansı, bellek yönetimi ve sistem sağlığı hakkında dilediğiniz soruyu sorabilirsiniz."; +"NVIDIA NIM System Analysis" = "NVIDIA NIM Sistem Analizi"; +"AI Report" = "Yapay Zeka Raporu"; +"Check for Updates" = "Güncellemeleri Denetle"; +"Reset DNS Cache" = "DNS Önbelleğini Sıfırla"; +"NVIDIA NIM connection error. Local suggestions are offered in the action buttons below." = "NVIDIA NIM bağlantı hatası. Yerel öneriler aşağıdaki eylem düğmelerinde sunuluyor."; +"Invalid Base URL format." = "Geçersiz Base URL formatı."; +"Security Warning: HTTPS is required for remote API connections." = "Güvenlik Uyarısı: Uzak API bağlantıları için HTTPS zorunludur."; +"Unsupported URL protocol. (Use https://)" = "Desteklenmeyen URL protokolü. (https:// kullanın)"; +"NVIDIA NIM API key is missing. Please enter your key." = "NVIDIA NIM API Anahtarı eksik. Lütfen anahtarınızı girin."; +"Invalid /models URL." = "Geçersiz /models URL'si."; +"Received an invalid HTTP response from the server." = "Sunucudan geçersiz HTTP yanıtı alındı."; +"NVIDIA NIM Error (%lld): %@" = "NVIDIA NIM Hatası (%lld): %@"; +"HTTP Error Code: %lld" = "HTTP Hata Kodu: %lld"; +"Could not parse the model list." = "Model listesi ayrıştırılamadı."; +"Model scanned via NVIDIA NIM." = "NVIDIA NIM üzerinden taranmış model."; +"No NVIDIA NIM API key entered. Please enter your key starting with 'nvapi-...'." = "NVIDIA NIM API Anahtarı girilmedi. Lütfen 'nvapi-...' ile başlayan anahtarınızı girin."; +"NVIDIA NIM connection and model ('%@') verified!" = "NVIDIA NIM bağlantısı ve model ('%@') doğrulandı!"; +"Connection error: %@" = "Bağlantı hatası: %@"; +"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ı."; diff --git a/Sources/MacOptimizer/Resources/tr.lproj/ai.stringsdict b/Sources/MacOptimizer/Resources/tr.lproj/ai.stringsdict new file mode 100644 index 0000000..98cfeff --- /dev/null +++ b/Sources/MacOptimizer/Resources/tr.lproj/ai.stringsdict @@ -0,0 +1,22 @@ + + + + %lld App Updates Available + NSStringLocalizedFormatKey%#@count@ + count + NSStringFormatSpecTypeKeyNSStringPluralRuleType + NSStringFormatValueTypeKeylld + one%lld Uygulama Güncellemesi Mevcut + other%lld Uygulama Güncellemesi Mevcut + + + NVIDIA NIM connection successful! %lld models scanned. + NSStringLocalizedFormatKey%#@count@ + count + NSStringFormatSpecTypeKeyNSStringPluralRuleType + NSStringFormatValueTypeKeylld + oneNVIDIA NIM bağlantısı başarılı! %lld adet model tarandı. + otherNVIDIA NIM bağlantısı başarılı! %lld adet model tarandı. + + + diff --git a/Sources/MacOptimizer/Services/AIAssistantService.swift b/Sources/MacOptimizer/Services/AIAssistantService.swift index 5460c06..0f819da 100644 --- a/Sources/MacOptimizer/Services/AIAssistantService.swift +++ b/Sources/MacOptimizer/Services/AIAssistantService.swift @@ -22,14 +22,16 @@ public actor AIAssistantService { config: NIMConfig) async -> (reply: String, actions: [AIAction]) { let lower = userMessage.lowercased() var actions: [AIAction] = [] - if lower.contains("çöp") || lower.contains("önbellek") || lower.contains("gereksiz") || lower.contains("temiz") { actions.append(AIAction(title: "Gereksiz Dosyaları Tara", type: .scanJunk)) } - if lower.contains("güncelle") || lower.contains("update") || lower.contains("yeni sürüm") { actions.append(AIAction(title: "Güncellemeleri Denetle", type: .checkUpdates)) } - if lower.contains("dns") || lower.contains("ağ") || lower.contains("internet") { actions.append(AIAction(title: "DNS Önbelleğini Sıfırla", type: .flushDNS)) } + // Keyword lists cover both Turkish and English user input. + if lower.contains("çöp") || lower.contains("önbellek") || lower.contains("gereksiz") || lower.contains("temiz") + || lower.contains("junk") || lower.contains("cache") || lower.contains("clean") { actions.append(AIAction(title: L10n.string("Scan for Junk Files", table: .ai), type: .scanJunk)) } + 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) return (reply, actions) } catch { - return ("NVIDIA NIM bağlantı hatası. Yerel öneriler aşağıdaki eylem düğmelerinde sunuluyor.", actions) + 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/NvidiaNIMService.swift b/Sources/MacOptimizer/Services/NvidiaNIMService.swift index 7e89492..f24adc9 100644 --- a/Sources/MacOptimizer/Services/NvidiaNIMService.swift +++ b/Sources/MacOptimizer/Services/NvidiaNIMService.swift @@ -26,7 +26,7 @@ public actor NvidiaNIMService { public nonisolated static func redactFilePaths(_ text: String) -> String { let pattern = #"(?:"|?*]+/)*[^\s<>:"|?*]+"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return text } - return regex.stringByReplacingMatches(in: text, range: NSRange(text.startIndex..., in: text), withTemplate: "[dosya yolu]") + return regex.stringByReplacingMatches(in: text, range: NSRange(text.startIndex..., in: text), withTemplate: "[file path]") } // MARK: - Safe Base URL Validator @@ -37,7 +37,7 @@ public actor NvidiaNIMService { } guard let url = URL(string: base), let scheme = url.scheme?.lowercased() else { - throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: "Geçersiz Base URL formatı."]) + throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: L10n.string("Invalid Base URL format.", table: .ai)]) } let host = url.host?.lowercased() ?? "" @@ -45,11 +45,11 @@ public actor NvidiaNIMService { // Disallow insecure HTTP over non-localhost to protect API keys in transit if scheme == "http" && !isLocalhost { - throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: "Güvenlik Uyarısı: Uzak API bağlantıları için HTTPS zorunludur."]) + throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: L10n.string("Security Warning: HTTPS is required for remote API connections.", table: .ai)]) } guard scheme == "https" || (scheme == "http" && isLocalhost) else { - throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: "Desteklenmeyen URL protokolü. (https:// kullanın)"]) + throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: L10n.string("Unsupported URL protocol. (Use https://)", table: .ai)]) } return base @@ -59,12 +59,12 @@ public actor NvidiaNIMService { public func fetchAvailableModels(config: NIMConfig) async throws -> [NIMModelOption] { let trimmedKey = config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedKey.isEmpty else { - throw NSError(domain: "NvidiaNIM", code: 401, userInfo: [NSLocalizedDescriptionKey: "NVIDIA NIM API Anahtarı eksik. Lütfen anahtarınızı girin."]) + throw NSError(domain: "NvidiaNIM", code: 401, userInfo: [NSLocalizedDescriptionKey: L10n.string("NVIDIA NIM API key is missing. Please enter your key.", table: .ai)]) } let base = try validateAndFormatBaseURL(config.baseURL) guard let url = URL(string: "\(base)/models") else { - throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: "Geçersiz /models URL'si."]) + throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: L10n.string("Invalid /models URL.", table: .ai)]) } var request = URLRequest(url: url) @@ -76,20 +76,20 @@ public actor NvidiaNIMService { let (data, response) = try await urlSession.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { - throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Sunucudan geçersiz HTTP yanıtı alındı."]) + throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: L10n.string("Received an invalid HTTP response from the server.", table: .ai)]) } if httpResponse.statusCode != 200 { if let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let message = (errorJson["error"] as? [String: Any])?["message"] as? String ?? errorJson["message"] as? String { - throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "NVIDIA NIM Hatası (\(httpResponse.statusCode)): \(message)"]) + throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: L10n.string("NVIDIA NIM Error (%lld): %@", table: .ai, httpResponse.statusCode, message)]) } - throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP Hata Kodu: \(httpResponse.statusCode)"]) + throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: L10n.string("HTTP Error Code: %lld", table: .ai, httpResponse.statusCode)]) } guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let dataArray = json["data"] as? [[String: Any]] else { - throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Model listesi ayrıştırılamadı."]) + throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: L10n.string("Could not parse the model list.", table: .ai)]) } var models: [NIMModelOption] = [] @@ -119,7 +119,7 @@ public actor NvidiaNIMService { id: modelId, name: "\(cleanName) (\(modelId))", provider: providerName, - description: "NVIDIA NIM üzerinden taranmış model.", + description: L10n.string("Model scanned via NVIDIA NIM.", table: .ai), isRecommended: isRec )) } @@ -136,22 +136,22 @@ public actor NvidiaNIMService { // MARK: - Test API Connection public func testConnection(config: NIMConfig) async -> (success: Bool, message: String) { guard !config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - return (false, "NVIDIA NIM API Anahtarı girilmedi. Lütfen 'nvapi-...' ile başlayan anahtarınızı girin.") + return (false, L10n.string("No NVIDIA NIM API key entered. Please enter your key starting with 'nvapi-...'.", table: .ai)) } // Try testing with models endpoint first, then chat probe do { let models = try await fetchAvailableModels(config: config) - return (true, "NVIDIA NIM bağlantısı başarılı! \(models.count) adet model tarandı.") + return (true, L10n.string("NVIDIA NIM connection successful! %lld models scanned.", table: .ai, models.count)) } catch { // Fallback to chat test let testPrompt = "Test" let messages = [["role": "user", "content": testPrompt]] do { - _ = try await sendChatCompletion(messages: messages, config: config, systemPrompt: "Kısa yanıt ver.") - return (true, "NVIDIA NIM bağlantısı ve model ('\(config.selectedModel)') doğrulandı!") + _ = try await sendChatCompletion(messages: messages, config: config, systemPrompt: "Reply briefly.") + return (true, L10n.string("NVIDIA NIM connection and model ('%@') verified!", table: .ai, config.selectedModel)) } catch { - return (false, "Bağlantı hatası: \(error.localizedDescription)") + return (false, L10n.string("Connection error: %@", table: .ai, error.localizedDescription)) } } } @@ -164,12 +164,12 @@ public actor NvidiaNIMService { ) async throws -> String { let trimmedKey = config.apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedKey.isEmpty else { - throw NSError(domain: "NvidiaNIM", code: 401, userInfo: [NSLocalizedDescriptionKey: "NVIDIA NIM API Anahtarı eksik."]) + throw NSError(domain: "NvidiaNIM", code: 401, userInfo: [NSLocalizedDescriptionKey: L10n.string("NVIDIA NIM API key is missing.", table: .ai)]) } let base = try validateAndFormatBaseURL(config.baseURL) guard let url = URL(string: "\(base)/chat/completions") else { - throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: "Geçersiz /chat/completions URL formatı."]) + throw NSError(domain: "NvidiaNIM", code: 400, userInfo: [NSLocalizedDescriptionKey: L10n.string("Invalid /chat/completions URL format.", table: .ai)]) } var allMessages: [[String: String]] = [] @@ -189,15 +189,15 @@ public actor NvidiaNIMService { let (data, response) = try await urlSession.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { - throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: "Sunucudan geçersiz HTTP yanıtı alındı."]) + throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: L10n.string("Received an invalid HTTP response from the server.", table: .ai)]) } if httpResponse.statusCode != 200 { if let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let message = (errorJson["error"] as? [String: Any])?["message"] as? String ?? errorJson["message"] as? String { - throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "NVIDIA NIM Hatası (\(httpResponse.statusCode)): \(message)"]) + throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: L10n.string("NVIDIA NIM Error (%lld): %@", table: .ai, httpResponse.statusCode, message)]) } - throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP Hata Kodu: \(httpResponse.statusCode)"]) + throw NSError(domain: "NvidiaNIM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: L10n.string("HTTP Error Code: %lld", table: .ai, httpResponse.statusCode)]) } guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], @@ -205,7 +205,7 @@ public actor NvidiaNIMService { let firstChoice = choices.first, let message = firstChoice["message"] as? [String: Any], let content = message["content"] as? String else { - throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: "API yanıtı ayrıştırılamadı."]) + throw NSError(domain: "NvidiaNIM", code: 500, userInfo: [NSLocalizedDescriptionKey: L10n.string("Could not parse the API response.", table: .ai)]) } return content.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Tests/MacOptimizerTests/MacOptimizerTests.swift b/Tests/MacOptimizerTests/MacOptimizerTests.swift index caabc17..0bcfb04 100644 --- a/Tests/MacOptimizerTests/MacOptimizerTests.swift +++ b/Tests/MacOptimizerTests/MacOptimizerTests.swift @@ -487,7 +487,7 @@ final class MacOptimizerTests: XCTestCase { XCTAssertTrue(insights.contains(where: { $0.severity == .critical })) let memoryInsight = insights.first(where: { $0.category == "RAM" }) XCTAssertTrue(memoryInsight?.actions.isEmpty == true) - XCTAssertTrue(memoryInsight?.summary.contains("uygulamaları kapatmayı") == true) + XCTAssertTrue(memoryInsight?.summary.contains(L10n.string("Try closing the apps using the most memory.", table: .ai)) == true) } func testNIMRequestPayloadDisclosureAndHistoryLimit() throws { @@ -495,18 +495,49 @@ final class MacOptimizerTests: XCTestCase { XCTAssertFalse(off.contains("SecretApp")) let on = NvidiaNIMProvider.cloudContext("metrics", processNames: ["SecretApp (PID: 7)"], includeProcesses: true) XCTAssertTrue(on.contains("SecretApp")) + XCTAssertTrue(on.contains("Running apps: SecretApp (PID: 7)")) let messages = (0..<10).map { ["role": "user", "content": "message-\($0)" + ($0 == 9 ? " /Users/alice/Secret.txt" : "")] } let body = try NvidiaNIMService.requestBody(messages: messages, config: NIMConfig(apiKey: "secret-key")) let payload = String(decoding: body, as: UTF8.self) XCTAssertFalse(payload.contains("secret-key")) XCTAssertFalse(payload.contains("/Users/")) - XCTAssertTrue(payload.contains("[dosya yolu]")) + XCTAssertTrue(payload.contains("[file path]")) XCTAssertFalse(payload.contains("message-0")) XCTAssertTrue(payload.contains("message-9")) let decoded = try JSONSerialization.jsonObject(with: body) as? [String: Any] XCTAssertEqual((decoded?["messages"] as? [[String: String]])?.count, 7) } + + func testNIMPromptsAreEnglishAndNameReplyLanguage() { + let system = NvidiaNIMProvider.copilotSystemPrompt(snapshotContext: "RAM 50%", replyLanguage: "Turkish") + XCTAssertTrue(system.hasPrefix("You are the MacOptimizer Pro macOS assistant.")) + XCTAssertTrue(system.contains("Reply in Turkish")) + XCTAssertTrue(system.contains("RAM 50%")) + XCTAssertTrue(NvidiaNIMProvider.diagnosisPrompt("metrics", replyLanguage: "English").contains("Reply in English.")) + + 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-TR"], forKey: "AppleLanguages") + XCTAssertEqual(L10n.currentLanguageEnglishName(), "Turkish") + UserDefaults.standard.set(["en"], forKey: "AppleLanguages") + XCTAssertEqual(L10n.currentLanguageEnglishName(), "English") + } + + func testCopilotKeywordsMatchTurkishAndEnglishInput() async { + let service = AIAssistantService() + let english = await service.chatWithCopilot(userMessage: "Please clean junk and check for update", history: [], + systemContext: "", config: NIMConfig()) + XCTAssertEqual(english.actions.map(\.type), [.scanJunk, .checkUpdates]) + 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) + } // MARK: - 11. Mach-O Architecture Detector Tests func testMachOArchitectureDetectorHeaders() {