语言切换立即生效:视图、应用菜单与菜单栏随设置重建,不再需要重启

This commit is contained in:
gentpan committed 2026-09-13 15:30:55 +05:00
1 parent 9af1aaaf27
commit 11fc451fae
12 files changed
+37 -27

No files matched your search

+4 -2
View File
@@ -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 <dir> --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 <dir> --language en` renders English screenshots and
writes any untranslated string to `untranslated.txt`.
`--snapshot <dir>` renders every main-window page, popover, settings section and the menu bar in light and
+1 -1
View File
@@ -21,7 +21,7 @@
- 蓝牙设备电量:本机信息页列出已连接的键盘、鼠标、触控板和耳机的电量(AirPods 分左耳、右耳、充电盒),低于 20% 标红;通知里新增“蓝牙设备电量低”(低于 15%)。
- 全局快捷键(设置 · 通用):可为显示 / 隐藏主窗口、打开进程页、开关防休眠、释放内存录制快捷键,在任何应用中都能用,不需要辅助功能权限;被其他应用占用时提示更换。
- 桌面小组件“系统概览”:小号显示 CPU、内存圆环与磁盘用量,中号显示 CPU、内存、磁盘、电池四个圆环和磁盘可用空间;在自己的沙盒进程里读取数据,主应用没开也能显示,刷新间隔由系统决定(几分钟一次)。
- 英文界面(设置 · 通用 · 语言):跟随系统、简体中文、English 三选一,系统首选语言不是中文时默认显示英文;约 880 条界面文案、通知、错误提示和 Apple 智能解释的回答都有英文版本,日期按英文格式显示,切换后重启生效。
- 英文界面(设置 · 通用 · 语言):跟随系统、简体中文、English 三选一,系统首选语言不是中文时默认显示英文;约 880 条界面文案、通知、错误提示和 Apple 智能解释的回答都有英文版本,日期按英文格式显示;切换后界面、菜单与菜单栏立即换成新语言,显示器、应用名称等由系统提供的文字在下次启动时切换。
#### 调整
@@ -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
@@ -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)
@@ -61,6 +61,8 @@ public struct MainWindowView: View {
.background(DS.Palette.background)
// 内容延伸到透明标题栏下方,由顶栏高度留出红绿灯按钮的位置
.ignoresSafeArea()
// 文案在各视图计算时翻译好,切换语言后整棵视图重建
.id(model.settings.language)
}
}
@@ -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 {
@@ -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)
}
}
@@ -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
}
@@ -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"
@@ -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"
@@ -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 }
@@ -80,7 +80,7 @@ public final class MaintenanceController {
}
}
nonisolated private static let cancelled = tr("已取消")
nonisolated private static var cancelled: String { tr("已取消") }
/// 未安装辅助工具时的回退:通过 AppleScript 请求一次性管理员授权执行命令。
/// 命令只由固定路径与已校验的参数拼成,这里再做 AppleScript 字符串转义