Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PackageDescription

let package = Package(
name: "ProcessBarMonitor",
defaultLocalization: "en",
platforms: [
.macOS(.v13)
],
Expand All @@ -18,7 +19,10 @@ let package = Package(
.executableTarget(
name: "ProcessBarMonitor",
dependencies: ["CSensors"],
path: "Sources/ProcessBarMonitor"
path: "Sources/ProcessBarMonitor",
resources: [
.process("Resources")
]
)
]
)
6 changes: 3 additions & 3 deletions Sources/ProcessBarMonitor/LaunchAtLoginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
54 changes: 54 additions & 0 deletions Sources/ProcessBarMonitor/Localization.swift
Original file line number Diff line number Diff line change
@@ -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<String>()
return candidates.filter { seen.insert($0).inserted }
}
}
26 changes: 14 additions & 12 deletions Sources/ProcessBarMonitor/MonitorViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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()
Expand All @@ -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")
}
}
}
10 changes: 5 additions & 5 deletions Sources/ProcessBarMonitor/ProcessSnapshotProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
59 changes: 59 additions & 0 deletions Sources/ProcessBarMonitor/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -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.";
Original file line number Diff line number Diff line change
@@ -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 温度来源。";
8 changes: 4 additions & 4 deletions Sources/ProcessBarMonitor/SystemMetricsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading
Loading