diff --git a/Package.swift b/Package.swift index a131ea1..2cf37e3 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,7 @@ import PackageDescription let package = Package( name: "ProcessBarMonitor", + defaultLocalization: "en", platforms: [ .macOS(.v13) ], @@ -18,7 +19,10 @@ let package = Package( .executableTarget( name: "ProcessBarMonitor", dependencies: ["CSensors"], - path: "Sources/ProcessBarMonitor" + path: "Sources/ProcessBarMonitor", + resources: [ + .process("Resources") + ] ) ] ) diff --git a/Sources/ProcessBarMonitor/LaunchAtLoginManager.swift b/Sources/ProcessBarMonitor/LaunchAtLoginManager.swift index 2cda1fa..bf777b1 100644 --- a/Sources/ProcessBarMonitor/LaunchAtLoginManager.swift +++ b/Sources/ProcessBarMonitor/LaunchAtLoginManager.swift @@ -55,11 +55,11 @@ enum LaunchAtLoginError: LocalizedError { var errorDescription: String? { switch self { case .unsupportedOS: - return "Launch at login requires macOS 13 or later." + return L10n.string("error.launch_at_login.unsupported") case .requiresApproval: - return "macOS accepted the login item request, but it may still need approval in System Settings → General → Login Items." + return L10n.string("error.launch_at_login.requires_approval") case .serviceFailure(let underlying): - return "Could not update the login item: \(underlying)" + return L10n.format("error.launch_at_login.service_failure", underlying) } } } diff --git a/Sources/ProcessBarMonitor/Localization.swift b/Sources/ProcessBarMonitor/Localization.swift new file mode 100644 index 0000000..33c59f2 --- /dev/null +++ b/Sources/ProcessBarMonitor/Localization.swift @@ -0,0 +1,54 @@ +import Foundation + +enum L10n { + private static let bundle: Bundle = { + let moduleBundle = Bundle.module + let available = Set(moduleBundle.localizations.map { $0.lowercased() }) + + for preferred in Locale.preferredLanguages.map({ $0.lowercased() }) { + let candidates = localizationCandidates(for: preferred) + for candidate in candidates where available.contains(candidate) { + if let path = moduleBundle.path(forResource: candidate, ofType: "lproj"), + let localizedBundle = Bundle(path: path) { + return localizedBundle + } + } + } + + return moduleBundle + }() + + static func string(_ key: String) -> String { + NSLocalizedString(key, bundle: bundle, comment: "") + } + + static func format(_ key: String, _ arguments: CVarArg...) -> String { + String(format: string(key), locale: Locale.current, arguments: arguments) + } + + private static func localizationCandidates(for language: String) -> [String] { + let parts = language.split(separator: "-").map(String.init) + guard !parts.isEmpty else { return [language] } + + var candidates: [String] = [] + candidates.append(language) + + if parts.count >= 2 { + candidates.append(parts[0] + "-" + parts[1]) + } + + candidates.append(parts[0]) + + if parts[0] == "zh" { + if language.contains("hans") { + candidates.insert("zh-hans", at: 0) + } + if language.contains("hant") { + candidates.insert("zh-hant", at: 0) + } + } + + var seen = Set() + return candidates.filter { seen.insert($0).inserted } + } +} diff --git a/Sources/ProcessBarMonitor/MonitorViewModel.swift b/Sources/ProcessBarMonitor/MonitorViewModel.swift index 60b1f73..792d9d4 100644 --- a/Sources/ProcessBarMonitor/MonitorViewModel.swift +++ b/Sources/ProcessBarMonitor/MonitorViewModel.swift @@ -65,14 +65,14 @@ final class MonitorViewModel: ObservableObject { processLimit = [5, 8, 12, 20].contains(savedProcessLimit) ? savedProcessLimit : 5 if let rawTemperatureMode = settings.string(forKey: Keys.temperatureMode), - let parsedTemperatureMode = TemperatureMode(rawValue: rawTemperatureMode) { + let parsedTemperatureMode = TemperatureMode(savedValue: rawTemperatureMode) { temperatureMode = parsedTemperatureMode } else { temperatureMode = .hottestCPU } if let rawMenuBarDisplayMode = settings.string(forKey: Keys.menuBarDisplayMode), - let parsedMenuBarDisplayMode = MenuBarDisplayMode(rawValue: rawMenuBarDisplayMode) { + let parsedMenuBarDisplayMode = MenuBarDisplayMode(savedValue: rawMenuBarDisplayMode) { menuBarDisplayMode = parsedMenuBarDisplayMode } else { menuBarDisplayMode = .compact @@ -86,11 +86,11 @@ final class MonitorViewModel: ObservableObject { switch menuBarDisplayMode { case .compact: - return "\(cpu) \(memoryUsed) \(temp)" + return L10n.format("menu_bar_title.compact", cpu, memoryUsed, temp) case .labeled: - return "CPU \(cpu) RAM \(memoryUsed) \(temp)" + return L10n.format("menu_bar_title.labeled", cpu, memoryUsed, temp) case .temperatureFirst: - return "\(temp) \(cpu) \(memoryUsed)" + return L10n.format("menu_bar_title.temperature_first", temp, cpu, memoryUsed) } } @@ -149,7 +149,7 @@ final class MonitorViewModel: ObservableObject { recomputeVisibleProcesses() statusMessage = nil case .failure(let error): - statusMessage = "Failed to load top apps: \(error.localizedDescription)" + statusMessage = L10n.format("status.failed_to_load_top_apps", error.localizedDescription) } } } @@ -194,7 +194,9 @@ final class MonitorViewModel: ObservableObject { func setLaunchAtLogin(_ enabled: Bool) { do { try launchAtLogin.setEnabled(enabled) - statusMessage = enabled ? "Launch at login enabled." : "Launch at login disabled." + statusMessage = enabled + ? L10n.string("status.launch_at_login_enabled") + : L10n.string("status.launch_at_login_disabled") } catch { statusMessage = error.localizedDescription launchAtLogin.refreshState() @@ -207,11 +209,11 @@ final class MonitorViewModel: ObservableObject { func thermalText(_ state: ProcessInfo.ThermalState) -> String { switch state { - case .nominal: return "Nominal" - case .fair: return "Fair" - case .serious: return "Serious" - case .critical: return "Critical" - @unknown default: return "Unknown" + case .nominal: return L10n.string("thermal.nominal") + case .fair: return L10n.string("thermal.fair") + case .serious: return L10n.string("thermal.serious") + case .critical: return L10n.string("thermal.critical") + @unknown default: return L10n.string("thermal.unknown") } } } diff --git a/Sources/ProcessBarMonitor/ProcessSnapshotProvider.swift b/Sources/ProcessBarMonitor/ProcessSnapshotProvider.swift index 848dfac..8c192b3 100644 --- a/Sources/ProcessBarMonitor/ProcessSnapshotProvider.swift +++ b/Sources/ProcessBarMonitor/ProcessSnapshotProvider.swift @@ -11,17 +11,17 @@ actor ProcessSnapshotProvider { var errorDescription: String? { switch self { case .launchFailed(let error): - return "Unable to start ps: \(error.localizedDescription)" + return L10n.format("error.ps.launch_failed", error.localizedDescription) case .commandFailed(let status, let stderr): let trimmed = stderr.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { - return "ps exited with status \(status)." + return L10n.format("error.ps.command_failed_no_stderr", status) } - return "ps exited with status \(status): \(trimmed)" + return L10n.format("error.ps.command_failed", status, trimmed) case .invalidOutputEncoding: - return "ps output was not valid UTF-8." + return L10n.string("error.ps.invalid_utf8") case .noProcessesParsed(let lineCount): - return "ps returned \(lineCount) lines, but none could be parsed." + return L10n.format("error.ps.no_processes_parsed", lineCount) } } } diff --git a/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings b/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..a2c9c13 --- /dev/null +++ b/Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings @@ -0,0 +1,59 @@ +"summary.cpu" = "CPU"; +"summary.ram" = "RAM"; +"summary.temp" = "Temp"; +"summary.thermal" = "Thermal"; +"metric.used_percent" = "%.0f%% used"; +"health.line" = "CPU %@ · RAM %@ · Temp %@"; +"trend.cpu" = "CPU Trend"; +"trend.ram" = "RAM Trend"; +"trend.temp" = "Temp Trend"; +"section.filter_display" = "Filter & Display"; +"picker.menu_bar" = "Menu Bar"; +"picker.temperature" = "Temperature"; +"search.placeholder" = "Search app name / path / pid / bundle id"; +"picker.rows" = "Rows"; +"toggle.launch_at_login" = "Launch at login"; +"section.top_apps_cpu" = "Top Apps by CPU"; +"section.top_apps_cpu_desc" = "Click an app to inspect grouped child processes. CPU uses raw per-process percentages, so totals can exceed 100%% on multicore Macs."; +"section.top_apps_memory" = "Top Apps by Memory"; +"section.top_apps_memory_desc" = "Click an app to inspect grouped child processes."; +"button.refreshing" = "Refreshing..."; +"button.refresh_now" = "Refresh Now"; +"button.quit" = "Quit"; +"label.updated_at" = "Updated %@"; +"header.app" = "App"; +"header.group" = "Group"; +"header.cpu" = "CPU"; +"header.memory" = "Memory"; +"process.more_count" = "+ %d more processes"; +"process.pid" = "pid %d"; +"process.count_procs" = "%d procs"; +"status.failed_to_load_top_apps" = "Failed to load top apps: %@"; +"status.launch_at_login_enabled" = "Launch at login enabled."; +"status.launch_at_login_disabled" = "Launch at login disabled."; +"thermal.nominal" = "Nominal"; +"thermal.fair" = "Fair"; +"thermal.serious" = "Serious"; +"thermal.critical" = "Critical"; +"thermal.unknown" = "Unknown"; +"menu_display.compact" = "Compact"; +"menu_display.labeled" = "Labeled"; +"menu_display.temperature_first" = "Temperature First"; +"menu_bar_title.compact" = "%@ %@ %@"; +"menu_bar_title.labeled" = "CPU %@ RAM %@ %@"; +"menu_bar_title.temperature_first" = "%@ %@ %@"; +"temp_mode.hottest_cpu" = "Hottest CPU"; +"temp_mode.average_cpu" = "Average CPU"; +"temp_mode.hottest_soc" = "Hottest SoC"; +"error.ps.launch_failed" = "Unable to start ps: %@"; +"error.ps.command_failed_no_stderr" = "ps exited with status %d."; +"error.ps.command_failed" = "ps exited with status %d: %@"; +"error.ps.invalid_utf8" = "ps output was not valid UTF-8."; +"error.ps.no_processes_parsed" = "ps returned %d lines, but none could be parsed."; +"error.launch_at_login.unsupported" = "Launch at login requires macOS 13 or later."; +"error.launch_at_login.requires_approval" = "macOS accepted the login item request, but it may still need approval in System Settings -> General -> Login Items."; +"error.launch_at_login.service_failure" = "Could not update the login item: %@"; +"note.temperature.available" = "Temperature mode: %@. Source: Apple Silicon HID sensors first, helper fallback second."; +"note.temperature.arm64_unavailable" = "Apple Silicon HID sensors were checked for %@, but no valid value was exposed right now."; +"note.temperature.intel_hint" = "Intel: install osx-cpu-temp or istats to show a real CPU temperature."; +"note.temperature.no_source" = "No valid CPU temperature source found."; diff --git a/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings b/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000..0734318 --- /dev/null +++ b/Sources/ProcessBarMonitor/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,59 @@ +"summary.cpu" = "CPU"; +"summary.ram" = "内存"; +"summary.temp" = "温度"; +"summary.thermal" = "热状态"; +"metric.used_percent" = "已用 %.0f%%"; +"health.line" = "CPU %@ · 内存 %@ · 温度 %@"; +"trend.cpu" = "CPU 趋势"; +"trend.ram" = "内存趋势"; +"trend.temp" = "温度趋势"; +"section.filter_display" = "筛选与显示"; +"picker.menu_bar" = "菜单栏"; +"picker.temperature" = "温度模式"; +"search.placeholder" = "搜索应用名 / 路径 / PID / Bundle ID"; +"picker.rows" = "行数"; +"toggle.launch_at_login" = "开机启动"; +"section.top_apps_cpu" = "CPU 占用最高应用"; +"section.top_apps_cpu_desc" = "点击应用可查看分组后的子进程。CPU 为各进程原始占比,多核 Mac 上总和可能超过 100%%。"; +"section.top_apps_memory" = "内存占用最高应用"; +"section.top_apps_memory_desc" = "点击应用可查看分组后的子进程。"; +"button.refreshing" = "刷新中..."; +"button.refresh_now" = "立即刷新"; +"button.quit" = "退出"; +"label.updated_at" = "更新于 %@"; +"header.app" = "应用"; +"header.group" = "分组"; +"header.cpu" = "CPU"; +"header.memory" = "内存"; +"process.more_count" = "+ 另外 %d 个进程"; +"process.pid" = "PID %d"; +"process.count_procs" = "%d 个进程"; +"status.failed_to_load_top_apps" = "加载 Top Apps 失败:%@"; +"status.launch_at_login_enabled" = "已启用开机启动。"; +"status.launch_at_login_disabled" = "已关闭开机启动。"; +"thermal.nominal" = "正常"; +"thermal.fair" = "一般"; +"thermal.serious" = "较高"; +"thermal.critical" = "严重"; +"thermal.unknown" = "未知"; +"menu_display.compact" = "紧凑"; +"menu_display.labeled" = "带标签"; +"menu_display.temperature_first" = "温度优先"; +"menu_bar_title.compact" = "%@ %@ %@"; +"menu_bar_title.labeled" = "CPU %@ 内存 %@ %@"; +"menu_bar_title.temperature_first" = "%@ %@ %@"; +"temp_mode.hottest_cpu" = "最高 CPU 温度"; +"temp_mode.average_cpu" = "平均 CPU 温度"; +"temp_mode.hottest_soc" = "最高 SoC 温度"; +"error.ps.launch_failed" = "无法启动 ps:%@"; +"error.ps.command_failed_no_stderr" = "ps 退出状态码 %d。"; +"error.ps.command_failed" = "ps 退出状态码 %d:%@"; +"error.ps.invalid_utf8" = "ps 输出不是有效的 UTF-8。"; +"error.ps.no_processes_parsed" = "ps 返回了 %d 行,但没有可解析的进程。"; +"error.launch_at_login.unsupported" = "开机启动需要 macOS 13 或更高版本。"; +"error.launch_at_login.requires_approval" = "macOS 已接受开机启动请求,但可能仍需在 系统设置 -> 通用 -> 登录项 中批准。"; +"error.launch_at_login.service_failure" = "无法更新登录项:%@"; +"note.temperature.available" = "温度模式:%@。来源:优先 Apple Silicon HID 传感器,其次为辅助工具。"; +"note.temperature.arm64_unavailable" = "已检查 %@ 的 Apple Silicon HID 传感器,但当前未返回有效值。"; +"note.temperature.intel_hint" = "Intel 机型:安装 osx-cpu-temp 或 istats 以显示真实 CPU 温度。"; +"note.temperature.no_source" = "未找到有效的 CPU 温度来源。"; diff --git a/Sources/ProcessBarMonitor/SystemMetricsProvider.swift b/Sources/ProcessBarMonitor/SystemMetricsProvider.swift index 6c173cf..9723cbc 100644 --- a/Sources/ProcessBarMonitor/SystemMetricsProvider.swift +++ b/Sources/ProcessBarMonitor/SystemMetricsProvider.swift @@ -117,14 +117,14 @@ actor SystemMetricsProvider { private func architectureAndTemperatureNote(temperatureAvailable: Bool, mode: TemperatureMode) -> String { if temperatureAvailable { - return "Temperature mode: \(mode.rawValue). Source: Apple Silicon HID sensors first, helper fallback second." + return L10n.format("note.temperature.available", mode.title) } #if arch(arm64) - return "Apple Silicon HID sensors were checked for \(mode.rawValue), but no valid value was exposed right now." + return L10n.format("note.temperature.arm64_unavailable", mode.title) #elseif arch(x86_64) - return "Intel: install osx-cpu-temp or istats to show a real CPU temperature." + return L10n.string("note.temperature.intel_hint") #else - return "No valid CPU temperature source found." + return L10n.string("note.temperature.no_source") #endif } } diff --git a/Sources/ProcessBarMonitor/SystemModels.swift b/Sources/ProcessBarMonitor/SystemModels.swift index a065683..86f15f6 100644 --- a/Sources/ProcessBarMonitor/SystemModels.swift +++ b/Sources/ProcessBarMonitor/SystemModels.swift @@ -1,19 +1,77 @@ import Foundation enum TemperatureMode: String, CaseIterable, Identifiable { - case hottestCPU = "Hottest CPU" - case averageCPU = "Average CPU" - case hottestSoC = "Hottest SoC" + case hottestCPU + case averageCPU + case hottestSoC var id: String { rawValue } + + var title: String { + switch self { + case .hottestCPU: + return L10n.string("temp_mode.hottest_cpu") + case .averageCPU: + return L10n.string("temp_mode.average_cpu") + case .hottestSoC: + return L10n.string("temp_mode.hottest_soc") + } + } + + init?(savedValue: String) { + if let mode = TemperatureMode(rawValue: savedValue) { + self = mode + return + } + + switch savedValue { + case "Hottest CPU": + self = .hottestCPU + case "Average CPU": + self = .averageCPU + case "Hottest SoC": + self = .hottestSoC + default: + return nil + } + } } enum MenuBarDisplayMode: String, CaseIterable, Identifiable { - case compact = "Compact" - case labeled = "Labeled" - case temperatureFirst = "Temperature First" + case compact + case labeled + case temperatureFirst var id: String { rawValue } + + var title: String { + switch self { + case .compact: + return L10n.string("menu_display.compact") + case .labeled: + return L10n.string("menu_display.labeled") + case .temperatureFirst: + return L10n.string("menu_display.temperature_first") + } + } + + init?(savedValue: String) { + if let mode = MenuBarDisplayMode(rawValue: savedValue) { + self = mode + return + } + + switch savedValue { + case "Compact": + self = .compact + case "Labeled": + self = .labeled + case "Temperature First": + self = .temperatureFirst + default: + return nil + } + } } struct ProcessChildStat: Identifiable, Hashable { @@ -54,7 +112,9 @@ struct ProcessStat: Identifiable, Hashable { } var pidSummary: String { - processCount > 1 ? "\(processCount) procs" : "pid \(pid)" + processCount > 1 + ? L10n.format("process.count_procs", processCount) + : L10n.format("process.pid", pid) } } diff --git a/Sources/ProcessBarMonitor/Views.swift b/Sources/ProcessBarMonitor/Views.swift index ddddffa..deab2f8 100644 --- a/Sources/ProcessBarMonitor/Views.swift +++ b/Sources/ProcessBarMonitor/Views.swift @@ -87,7 +87,7 @@ struct ProcessRowView: View { .font(.system(size: 11, design: .rounded)) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) - Text("pid \(child.pid)") + Text(L10n.format("process.pid", child.pid)) .font(.system(size: 10, design: .monospaced)) .foregroundStyle(.secondary) .frame(width: 72, alignment: .leading) @@ -101,7 +101,7 @@ struct ProcessRowView: View { } if process.childProcesses.count > 8 { - Text("+ \(process.childProcesses.count - 8) more processes") + Text(L10n.format("process.more_count", process.childProcesses.count - 8)) .font(.caption2) .foregroundStyle(.secondary) } @@ -143,7 +143,7 @@ struct MenuBarContentView: View { } private var memoryCompact: String { - String(format: "%.0f%% used", viewModel.summary.memoryPressurePercent) + L10n.format("metric.used_percent", viewModel.summary.memoryPressurePercent) } private var currentTemperatureColor: Color { @@ -155,17 +155,17 @@ struct MenuBarContentView: View { private var healthLine: String { let temp = viewModel.summary.cpuTemperatureC.map { String(format: "%.1f°C", $0) } ?? "--" - return "CPU \(String(format: "%.0f%%", viewModel.summary.cpuPercent)) · RAM \(memoryCompact) · Temp \(temp)" + return L10n.format("health.line", String(format: "%.0f%%", viewModel.summary.cpuPercent), memoryCompact, temp) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 8) { - SummaryCardView(title: "CPU", value: String(format: "%.1f %%", viewModel.summary.cpuPercent), accent: .primary) - SummaryCardView(title: "RAM", value: memorySummary, accent: .blue) - SummaryCardView(title: "Temp", value: viewModel.summary.cpuTemperatureC.map { String(format: "%.1f °C", $0) } ?? "--", accent: currentTemperatureColor) - SummaryCardView(title: "Thermal", value: viewModel.thermalText(viewModel.summary.thermalState), accent: .pink) + SummaryCardView(title: L10n.string("summary.cpu"), value: String(format: "%.1f %%", viewModel.summary.cpuPercent), accent: .primary) + SummaryCardView(title: L10n.string("summary.ram"), value: memorySummary, accent: .blue) + SummaryCardView(title: L10n.string("summary.temp"), value: viewModel.summary.cpuTemperatureC.map { String(format: "%.1f °C", $0) } ?? "--", accent: currentTemperatureColor) + SummaryCardView(title: L10n.string("summary.thermal"), value: viewModel.thermalText(viewModel.summary.thermalState), accent: .pink) } VStack(alignment: .leading, spacing: 4) { @@ -179,7 +179,7 @@ struct MenuBarContentView: View { HStack(spacing: 8) { SparklineView( - title: "CPU Trend", + title: L10n.string("trend.cpu"), points: viewModel.cpuHistory, color: .primary, valueText: String(format: "%.1f%%", viewModel.summary.cpuPercent), @@ -188,7 +188,7 @@ struct MenuBarContentView: View { criticalThreshold: 85 ) SparklineView( - title: "RAM Trend", + title: L10n.string("trend.ram"), points: viewModel.memoryHistory, color: .blue, valueText: String(format: "%.0f%%", viewModel.summary.memoryPressurePercent), @@ -197,7 +197,7 @@ struct MenuBarContentView: View { criticalThreshold: 90 ) SparklineView( - title: "Temp Trend", + title: L10n.string("trend.temp"), points: viewModel.temperatureHistory, color: .green, valueText: viewModel.summary.cpuTemperatureC.map { String(format: "%.1f°C", $0) } ?? "--", @@ -210,32 +210,32 @@ struct MenuBarContentView: View { Divider() VStack(alignment: .leading, spacing: 8) { - Text("Filter & Display") + Text(L10n.string("section.filter_display")) .font(.headline) - Picker("Menu Bar", selection: $viewModel.menuBarDisplayMode) { + Picker(L10n.string("picker.menu_bar"), selection: $viewModel.menuBarDisplayMode) { ForEach(MenuBarDisplayMode.allCases) { mode in - Text(mode.rawValue).tag(mode) + Text(mode.title).tag(mode) } } .pickerStyle(.segmented) - Picker("Temperature", selection: $viewModel.temperatureMode) { + Picker(L10n.string("picker.temperature"), selection: $viewModel.temperatureMode) { ForEach(TemperatureMode.allCases) { mode in - Text(mode.rawValue).tag(mode) + Text(mode.title).tag(mode) } } .onChange(of: viewModel.temperatureMode) { _ in Task { await viewModel.refresh(forceProcesses: true) } } - TextField("Search app name / path / pid / bundle id", text: $viewModel.searchText) + TextField(L10n.string("search.placeholder"), text: $viewModel.searchText) .textFieldStyle(.roundedBorder) .onChange(of: viewModel.searchText) { _ in viewModel.recomputeVisibleProcesses() } - Picker("Rows", selection: $viewModel.processLimit) { + Picker(L10n.string("picker.rows"), selection: $viewModel.processLimit) { Text("5").tag(5) Text("8").tag(8) Text("12").tag(12) @@ -247,7 +247,7 @@ struct MenuBarContentView: View { } } - Toggle("Launch at login", isOn: Binding( + Toggle(L10n.string("toggle.launch_at_login"), isOn: Binding( get: { viewModel.launchAtLogin.isEnabled }, set: { newValue in viewModel.setLaunchAtLogin(newValue) @@ -267,9 +267,9 @@ struct MenuBarContentView: View { Divider() VStack(alignment: .leading, spacing: 4) { - Text("Top Apps by CPU") + Text(L10n.string("section.top_apps_cpu")) .font(.headline) - Text("Click an app to inspect grouped child processes. CPU uses raw per-process percentages, so totals can exceed 100% on multicore Macs.") + Text(L10n.string("section.top_apps_cpu_desc")) .font(.caption) .foregroundStyle(.secondary) processHeader @@ -288,9 +288,9 @@ struct MenuBarContentView: View { Divider() VStack(alignment: .leading, spacing: 4) { - Text("Top Apps by Memory") + Text(L10n.string("section.top_apps_memory")) .font(.headline) - Text("Click an app to inspect grouped child processes.") + Text(L10n.string("section.top_apps_memory_desc")) .font(.caption) .foregroundStyle(.secondary) processHeader @@ -310,17 +310,17 @@ struct MenuBarContentView: View { HStack { Button(action: { Task { await viewModel.refresh(forceProcesses: true) } }) { - Text(viewModel.isRefreshing ? "Refreshing..." : "Refresh Now") + Text(viewModel.isRefreshing ? L10n.string("button.refreshing") : L10n.string("button.refresh_now")) } .disabled(viewModel.isRefreshing) - Button("Quit") { + Button(L10n.string("button.quit")) { quitApplication() } Spacer() - Text("Updated \(viewModel.summary.updatedAt.formatted(date: .omitted, time: .standard))") + Text(L10n.format("label.updated_at", viewModel.summary.updatedAt.formatted(date: .omitted, time: .standard))) .font(.caption2) .foregroundStyle(.secondary) } @@ -338,19 +338,19 @@ struct MenuBarContentView: View { private var processHeader: some View { HStack(spacing: 8) { - Text("App") + Text(L10n.string("header.app")) .font(.caption) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) - Text("Group") + Text(L10n.string("header.group")) .font(.caption) .foregroundStyle(.secondary) .frame(width: 72, alignment: .leading) - Text("CPU") + Text(L10n.string("header.cpu")) .font(.caption) .foregroundStyle(.secondary) .frame(width: 56, alignment: .trailing) - Text("Memory") + Text(L10n.string("header.memory")) .font(.caption) .foregroundStyle(.secondary) .frame(width: 66, alignment: .trailing) diff --git a/build_app.sh b/build_app.sh index bccc7c0..cc8fd16 100755 --- a/build_app.sh +++ b/build_app.sh @@ -9,14 +9,19 @@ ROOT="$(cd "$(dirname "$0")" && pwd)" APP_DIR="$ROOT/dist/$APP_NAME.app" MACOS_DIR="$APP_DIR/Contents/MacOS" RESOURCES_DIR="$APP_DIR/Contents/Resources" +BUILD_DIR="$ROOT/.build/arm64-apple-macosx/debug" +RESOURCE_BUNDLE="$BUILD_DIR/${APP_NAME}_${APP_NAME}.bundle" cd "$ROOT" swift scripts/generate_icon.swift swift build rm -rf "$APP_DIR" mkdir -p "$MACOS_DIR" "$RESOURCES_DIR" -cp "$ROOT/.build/debug/$APP_NAME" "$MACOS_DIR/$APP_NAME" +cp "$BUILD_DIR/$APP_NAME" "$MACOS_DIR/$APP_NAME" cp "$ROOT/Resources/ProcessBarMonitor.icns" "$RESOURCES_DIR/ProcessBarMonitor.icns" +if [ -d "$RESOURCE_BUNDLE" ]; then + cp -R "$RESOURCE_BUNDLE" "$RESOURCES_DIR/$(basename "$RESOURCE_BUNDLE")" +fi cat > "$APP_DIR/Contents/Info.plist" <