diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 862a6a4..83f67d5 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -174,8 +174,10 @@ Source strings are Simplified Chinese. `Scripts/l10n_wrap.py` wraps every Chines
`tr` returns the input unless English is active; otherwise it looks the text up in
`Localization/Translations.swift` — exact keys first, then templates where `{}` stands for an
interpolated value, longest literal fragments first, translating captured values once more. The
-language is chosen at launch (System follows the global `AppleLanguages`) and applies after a
-relaunch; dates use `L10n.locale`. `--snapshot
--language en` renders English screenshots and
+language is configured at launch (System follows the global `AppleLanguages`). Changing it
+reconfigures `L10n`, rebuilds the main window and update prompt through `.id(language)`, rebuilds the
+app menu and redraws the menu bar; popovers are created on open. Names supplied by macOS follow the
+app's `AppleLanguages`, which changes at the next launch. Dates use `L10n.locale`. `--snapshot --language en` renders English screenshots and
writes any untranslated string to `untranslated.txt`.
`--snapshot ` renders every main-window page, popover, settings section and the menu bar in light and
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36d4620..18effe0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,7 @@
- 蓝牙设备电量:本机信息页列出已连接的键盘、鼠标、触控板和耳机的电量(AirPods 分左耳、右耳、充电盒),低于 20% 标红;通知里新增“蓝牙设备电量低”(低于 15%)。
- 全局快捷键(设置 · 通用):可为显示 / 隐藏主窗口、打开进程页、开关防休眠、释放内存录制快捷键,在任何应用中都能用,不需要辅助功能权限;被其他应用占用时提示更换。
- 桌面小组件“系统概览”:小号显示 CPU、内存圆环与磁盘用量,中号显示 CPU、内存、磁盘、电池四个圆环和磁盘可用空间;在自己的沙盒进程里读取数据,主应用没开也能显示,刷新间隔由系统决定(几分钟一次)。
-- 英文界面(设置 · 通用 · 语言):跟随系统、简体中文、English 三选一,系统首选语言不是中文时默认显示英文;约 880 条界面文案、通知、错误提示和 Apple 智能解释的回答都有英文版本,日期按英文格式显示,切换后重启生效。
+- 英文界面(设置 · 通用 · 语言):跟随系统、简体中文、English 三选一,系统首选语言不是中文时默认显示英文;约 880 条界面文案、通知、错误提示和 Apple 智能解释的回答都有英文版本,日期按英文格式显示;切换后界面、菜单与菜单栏立即换成新语言,显示器、应用名称等由系统提供的文字在下次启动时切换。
#### 调整
diff --git a/Packages/OpenStatsKit/Sources/Localization/Translations.swift b/Packages/OpenStatsKit/Sources/Localization/Translations.swift
index 85e2bba..6b2778b 100644
--- a/Packages/OpenStatsKit/Sources/Localization/Translations.swift
+++ b/Packages/OpenStatsKit/Sources/Localization/Translations.swift
@@ -888,8 +888,7 @@ npm 缓存 npm Cache
,{} 项未能移动 , {} couldn't be moved
,已从程序坞移除 , removed from the Dock
:{} : {}
-重新启动 OpenStats 后生效 Takes effect after OpenStats relaunches
-立即重启 Relaunch Now
+立即切换;显示器、应用名称等由系统提供的文字在下次启动时切换 Applies right away; names supplied by macOS, such as displays and apps, switch at next launch
1 项 1 item
1 个进程 1 process
1 个应用,系统自带的不列出 1 app; built-in apps are not listed
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift
index 53315cd..34e8c62 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/AppController.swift
@@ -53,6 +53,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
observeProbeSettings()
applyAppearance()
updateNetworkVisibility()
+ appliedLanguage = model.settings.language
Task { await model.geo.updateIfNeeded() }
startUpdateChecks()
model.alerts.openTab = { [weak self] tab in
@@ -185,9 +186,11 @@ public final class AppController: NSObject, NSApplicationDelegate {
_ = model.settings.probeEnabled
_ = model.settings.probeInBackground
_ = model.settings.enabledAlerts
+ _ = model.settings.language
} onChange: { [weak self] in
Task { @MainActor in
guard let self else { return }
+ self.applyLanguageIfChanged()
self.model.alerts.applySettings()
await self.model.hub.update(self.model.demand)
self.applyAppearance()
@@ -219,6 +222,22 @@ public final class AppController: NSObject, NSApplicationDelegate {
}
}
+ private var appliedLanguage: AppLanguage?
+
+ /// 切换语言后重建应用菜单、菜单栏图标文字与已打开的弹窗
+ private func applyLanguageIfChanged() {
+ let language = model.settings.language
+ guard appliedLanguage != nil, appliedLanguage != language else {
+ appliedLanguage = language
+ return
+ }
+ appliedLanguage = language
+ NSApp.mainMenu = MainMenu.make(target: self, settingsAction: #selector(openSettingsFromMenu),
+ updateAction: #selector(checkForUpdatesFromMenu))
+ menuBar.dismissPopovers()
+ menuBar.refreshImages()
+ }
+
private func updateNetworkVisibility() {
model.network.setVisibility(inMenuBar: model.settings.menuBarItems.contains(.network),
detailVisible: model.isNetworkDetailVisible)
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift
index b47da11..e0a302b 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/MainWindowView.swift
@@ -61,6 +61,8 @@ public struct MainWindowView: View {
.background(DS.Palette.background)
// 内容延伸到透明标题栏下方,由顶栏高度留出红绿灯按钮的位置
.ignoresSafeArea()
+ // 文案在各视图计算时翻译好,切换语言后整棵视图重建
+ .id(model.settings.language)
}
}
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift
index d12bac3..54d85f7 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift
@@ -81,18 +81,10 @@ struct GeneralSettings: View {
}
}
GroupRow {
- SettingRow(title: "语言 / Language",
- subtitle: settings.language.resolved == L10n.language ? nil : tr("重新启动 OpenStats 后生效")) {
- HStack(spacing: DS.Space.s2) {
- if settings.language.resolved != L10n.language {
- Button(tr("立即重启")) { model.relaunch() }
- .buttonStyle(DSButtonStyle(kind: .primary))
- .fixedSize()
- }
- SegmentedControl(selection: $settings.language,
- options: AppLanguage.allCases.map { ($0, $0.title) })
- .frame(width: DS.Size.sidebarWidth + DS.Space.s12)
- }
+ SettingRow(title: "语言 / Language", subtitle: tr("立即切换;显示器、应用名称等由系统提供的文字在下次启动时切换")) {
+ SegmentedControl(selection: $settings.language,
+ options: AppLanguage.allCases.map { ($0, $0.title) })
+ .frame(width: DS.Size.sidebarWidth + DS.Space.s12)
}
}
GroupRow {
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/UpdateViews.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/UpdateViews.swift
index af1c662..2e1360a 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/UpdateViews.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/UpdateViews.swift
@@ -138,6 +138,7 @@ struct UpdatePromptView: View {
.padding(.top, DS.Size.windowHeader)
.frame(width: DS.Size.updateWindowWidth)
.background(DS.Palette.background)
+ .id(model.settings.language)
}
}
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift
index 837ec11..7de56c1 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppModel.swift
@@ -97,12 +97,6 @@ public final class AppModel {
openPopover == .network || (isMainWindowVisible && settings.panelTab == .network)
}
- /// 等当前进程退出后重新打开应用(切换语言后使用)
- func relaunch() {
- try? UpdateInstaller.relaunch(Bundle.main.bundleURL)
- quit()
- }
-
func refreshLaunchAtLogin() {
launchAtLoginEnabled = SMAppService.mainApp.status == .enabled
}
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
index d0c5d21..585ecc9 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
@@ -404,11 +404,12 @@ public final class AppSettings {
public var alertCPUTemperature: Int {
didSet { defaults.set(alertCPUTemperature, forKey: Keys.alertCPUTemperature) }
}
- /// 界面语言,重启后生效
+ /// 界面语言:界面文字立即切换;系统提供的名称(显示器、应用名)在下次启动时跟着切换
public var language: AppLanguage {
didSet {
defaults.set(language.rawValue, forKey: Keys.language)
language.applyToProcessLocale(defaults: defaults)
+ L10n.configure(language)
}
}
nonisolated public static let languageKey = "language"
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/GeoDatabaseController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/GeoDatabaseController.swift
index e3fa149..69cc4b9 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/GeoDatabaseController.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/GeoDatabaseController.swift
@@ -25,7 +25,7 @@ public final class GeoDatabaseController {
@ObservationIgnored private var asnDatabase: MaxMindDatabase?
static let manifestURL = URL(string: "https://getopenstats.com/geoip/manifest.json")!
- static let attribution = tr("本产品包含 MaxMind 创建的 GeoLite2 数据,可从 https://www.maxmind.com 获取。")
+ static var attribution: String { tr("本产品包含 MaxMind 创建的 GeoLite2 数据,可从 https://www.maxmind.com 获取。") }
private static let checkInterval: TimeInterval = 3 * 24 * 3600
private static let lastCheckedKey = "geoDatabaseLastChecked"
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/HelperClient.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/HelperClient.swift
index 702d60d..3e8e3b2 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/HelperClient.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/HelperClient.swift
@@ -40,7 +40,7 @@ public final class HelperClient {
public var isReady: Bool { status == .enabled }
- static let outdatedMessage = tr("辅助工具版本比应用旧,部分功能可能无法使用,请重新安装一次(需要管理员授权)。")
+ static var outdatedMessage: String { tr("辅助工具版本比应用旧,部分功能可能无法使用,请重新安装一次(需要管理员授权)。") }
/// 未安装、待批准或版本过旧,需要用户处理
var needsAttention: Bool { !isReady || isOutdated }
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/MaintenanceController.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/MaintenanceController.swift
index 2b489ae..80a5367 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/MaintenanceController.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/MaintenanceController.swift
@@ -80,7 +80,7 @@ public final class MaintenanceController {
}
}
- nonisolated private static let cancelled = tr("已取消")
+ nonisolated private static var cancelled: String { tr("已取消") }
/// 未安装辅助工具时的回退:通过 AppleScript 请求一次性管理员授权执行命令。
/// 命令只由固定路径与已校验的参数拼成,这里再做 AppleScript 字符串转义