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 字符串转义