Fix explicit Chinese localization in window chrome

This commit is contained in:
Kaede Akino committed 2026-08-30 08:45:22 +08:00
1 parent 8086a9142f
commit 2ab1fdd5fb
14 files changed
+220 -96

No files matched your search

+5 -1
View File
@@ -10,6 +10,7 @@ import SwiftUI
struct RootView: View {
@Environment(AppModel.self) private var app
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.locale) private var locale
@State private var window: WindowModel
@State private var layoutWidth = Metrics.Window.defaultWidth
@State private var automaticQuickOpenReady = false
@@ -74,7 +75,10 @@ struct RootView: View {
.background { FullHeightWindowContent() }
.onGeometryChange(for: CGFloat.self) { $0.size.width } action: { layoutWidth = $0 }
.toolbarBackground(.hidden, for: .windowToolbar)
.navigationTitle(WindowTitleCatalog.standard(hosts: app.hostLibrary).title(in: window))
.navigationTitle(
WindowTitleCatalog.standard(hosts: app.hostLibrary)
.title(in: window, locale: locale)
)
// The window's only toolbar. Creation on the left, one Find on the
// right, both following the content in front rather than the sidebar —
// see `WindowToolbarLeadingActions` and `WindowModel.findKind`.
@@ -119,7 +119,7 @@ final class AppCommandRegistry {
VaultPage.allCases.map { page in
AppCommandDescriptor(
id: "vault.\(page.rawValue)",
title: page.title,
title: page.title(),
subtitle: String(localized: "Vault"),
symbol: page.symbol,
section: .navigation,
+23 -11
View File
@@ -264,6 +264,7 @@ private struct AssistantConversationList: View {
typealias Scope = AssistantLibraryModel.SearchScope
@Environment(WindowModel.self) private var window
@Environment(\.locale) private var locale
@AppStorage(SettingsKey.pinnedChatsSectionExpanded) private var pinnedExpanded = true
@AppStorage(SettingsKey.chatsSectionExpanded) private var chatsExpanded = true
@State private var deleteTarget: ConversationModel?
@@ -334,6 +335,7 @@ private struct AssistantConversationList: View {
// object of type WindowModel found". The list is handed the same
// reference so it can put it back into each row's hosting view.
let window = window
let locale = locale
return AssistantConversationNativeList(
items: activeItems,
@@ -351,7 +353,7 @@ private struct AssistantConversationList: View {
)
window.dragging = nil
},
blankAreaMenu: { Self.blankAreaMenu(in: window) }
blankAreaMenu: { Self.blankAreaMenu(in: window, locale: locale) }
) { item in
renderedItem(item, window: window)
}
@@ -360,13 +362,14 @@ private struct AssistantConversationList: View {
private var archivedList: some View {
let window = window
let locale = locale
return AssistantConversationNativeList(
items: archivedItems,
window: window,
topInset: Metrics.Space.m,
bottomInset: Metrics.Space.m,
blankAreaMenu: { Self.blankAreaMenu(in: window) }
blankAreaMenu: { Self.blankAreaMenu(in: window, locale: locale) }
) { item in
renderedItem(item, window: window)
.frame(maxWidth: archiveMaxWidth ?? .infinity)
@@ -382,13 +385,17 @@ private struct AssistantConversationList: View {
///
/// Static, and given the window rather than reading it: AppKit asks for this
/// when the pointer is already down, long after the body that built the list.
private static func blankAreaMenu(in window: WindowModel) -> [NativeMenuEntry] {
private static func blankAreaMenu(
in window: WindowModel, locale: Locale
) -> [NativeMenuEntry] {
[
NativeMenuEntry(title: String(localized: "New Chat")) {
NativeMenuEntry(
title: AppLocalization.string("New Chat", locale: locale)
) {
window.openNewAssistantChat()
},
NativeMenuEntry(
title: String(localized: "Show Archive"),
title: AppLocalization.string("Show Archive", locale: locale),
isOn: window.isAssistantArchivePresented
) { window.showAssistantArchive() },
]
@@ -1131,6 +1138,7 @@ struct AssistantConversationRow: View {
@Environment(WindowModel.self) private var window
@Environment(\.controlActiveState) private var controlActiveState
@Environment(\.locale) private var locale
@State private var hovering = false
@FocusState private var titleFocused: Bool
@@ -1182,7 +1190,7 @@ struct AssistantConversationRow: View {
private var contextMenu: [NativeMenuEntry] {
let managementDisabled = conversation.isStreaming || conversation.isCompacting
var entries: [NativeMenuEntry] = [
NativeMenuEntry(title: String(localized: "Open Full Screen")) {
NativeMenuEntry(title: localized("Open Full Screen")) {
window.openAssistantFullScreen(conversation.id)
},
.separator,
@@ -1191,18 +1199,18 @@ struct AssistantConversationRow: View {
entries.append(
NativeMenuEntry(
title: conversation.isPinned
? String(localized: "Unpin") : String(localized: "Pin"),
? localized("Unpin") : localized("Pin"),
isEnabled: !managementDisabled
) { window.toggleAssistantPin(conversation.id) }
)
}
entries.append(
NativeMenuEntry(title: String(localized: "Rename"), action: onBeginRename)
NativeMenuEntry(title: localized("Rename"), action: onBeginRename)
)
entries.append(
NativeMenuEntry(
title: conversation.isArchived
? String(localized: "Unarchive") : String(localized: "Archive"),
? localized("Unarchive") : localized("Archive"),
isEnabled: conversation.isArchived || !managementDisabled
) {
window.setAssistantArchived(
@@ -1212,7 +1220,7 @@ struct AssistantConversationRow: View {
)
entries.append(.separator)
entries.append(
NativeMenuEntry(title: String(localized: "Delete"), isDestructive: true) {
NativeMenuEntry(title: localized("Delete"), isDestructive: true) {
onDelete()
}
)
@@ -1262,11 +1270,15 @@ struct AssistantConversationRow: View {
.buttonStyle(.plain)
.help(Text(
conversation.isArchived
? String(localized: "Unarchive") : String(localized: "Archive")
? localized("Unarchive") : localized("Archive")
))
}
}
}
private func localized(_ key: String.LocalizationValue) -> String {
AppLocalization.string(key, locale: locale)
}
}
/// The second pinned destination's main conversation surface.
@@ -31,11 +31,11 @@ nonisolated enum HostConnectionProtocol: String, CaseIterable, Identifiable, Sen
nonisolated var id: Self { self }
var displayName: String {
func displayName(locale: Locale = .current) -> String {
switch self {
case .ssh: String(localized: "SSH")
case .sftp: String(localized: "SFTP")
case .serial: String(localized: "Serial")
case .ssh: AppLocalization.string("SSH", locale: locale)
case .sftp: AppLocalization.string("SFTP", locale: locale)
case .serial: AppLocalization.string("Serial", locale: locale)
}
}
@@ -159,6 +159,7 @@ struct HostChooserView: View {
@Environment(AppModel.self) private var app
@Environment(WindowModel.self) private var window
@Environment(\.dismiss) private var dismiss
@Environment(\.locale) private var locale
@State private var chooser: ManagedChooserState<Host.ID>
@State private var connectionProtocol = HostConnectionProtocol.ssh
@State private var serialConfiguration = SerialConnectionConfiguration()
@@ -260,7 +261,7 @@ struct HostChooserView: View {
private var connectionProtocolPicker: some View {
Picker("Connection Protocol", selection: $connectionProtocol) {
ForEach(availableConnectionProtocols) { connectionProtocol in
Text(connectionProtocol.displayName).tag(connectionProtocol)
Text(connectionProtocol.displayName(locale: locale)).tag(connectionProtocol)
}
}
.pickerStyle(.segmented)
@@ -390,12 +391,16 @@ struct HostChooserView: View {
private var footer: some View {
HStack {
if purpose.allowsMultiple, connectionProtocol != .serial {
Text("\(chooser.selected.count) selected")
Text(
AppLocalization.string(
"\(chooser.selected.count) selected", locale: locale
)
)
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
Button(allResultsChecked ? "Deselect All" : "Select All") {
Button(selectAllLabel) {
toggleAllResults()
}
.disabled(results.isEmpty)
@@ -463,11 +468,15 @@ struct HostChooserView: View {
let path = serialConfiguration.normalizedDevicePath
var info = stat()
guard lstat(path, &info) == 0 else {
serialError = String(localized: "The device path does not exist.")
serialError = AppLocalization.string(
"The device path does not exist.", locale: locale
)
return
}
guard (info.st_mode & S_IFMT) == S_IFCHR else {
serialError = String(localized: "The device path is not a character device.")
serialError = AppLocalization.string(
"The device path is not a character device.", locale: locale
)
return
}
serialConfiguration.devicePath = path
@@ -546,12 +555,28 @@ struct HostChooserView: View {
private var workspaceHelp: Text {
if chooser.selected.count < 2 {
Text("Select at least two hosts. A Workspace can contain one or more hosts.")
Text(
AppLocalization.string(
"Select at least two hosts. A Workspace can contain one or more hosts.",
locale: locale
)
)
} else {
Text("Open \(chooser.selected.count) hosts as panes in one workspace.")
Text(
AppLocalization.string(
"Open \(chooser.selected.count) hosts as panes in one workspace.",
locale: locale
)
)
}
}
private var selectAllLabel: String {
AppLocalization.string(
allResultsChecked ? "Deselect All" : "Select All", locale: locale
)
}
private func move(_ delta: Int) -> KeyPress.Result {
guard !results.isEmpty else { return .ignored }
chooser.moveCursor(by: delta, through: results.map(\.id))
+5 -5
View File
@@ -19,12 +19,12 @@ nonisolated enum VaultPage: String, Hashable, Sendable, CaseIterable, Identifiab
}
}
var title: String {
func title(locale: Locale = .current) -> String {
switch self {
case .welcome: String(localized: "Welcome")
case .hosts: String(localized: "Hosts")
case .keychain: String(localized: "Keychain")
case .snippets: String(localized: "Snippets")
case .welcome: AppLocalization.string("Welcome", locale: locale)
case .hosts: AppLocalization.string("Hosts", locale: locale)
case .keychain: AppLocalization.string("Keychain", locale: locale)
case .snippets: AppLocalization.string("Snippets", locale: locale)
}
}
+18 -13
View File
@@ -10,17 +10,19 @@ import Foundation
/// window only asks, and gains nothing to update when a new destination is added.
protocol WindowTitleProviding {
/// nil when the destination in front is not this provider's to name.
func title(in window: WindowModel) -> String?
func title(in window: WindowModel, locale: Locale) -> String?
}
struct AnyWindowTitleProvider: WindowTitleProviding {
private let resolve: (WindowModel) -> String?
private let resolve: (WindowModel, Locale) -> String?
init<P: WindowTitleProviding>(_ provider: P) {
resolve = provider.title(in:)
resolve = provider.title(in:locale:)
}
func title(in window: WindowModel) -> String? { resolve(window) }
func title(in window: WindowModel, locale: Locale) -> String? {
resolve(window, locale)
}
}
struct WindowTitleCatalog {
@@ -32,8 +34,8 @@ struct WindowTitleCatalog {
/// The first provider that claims the destination. Empty when none does,
/// which AppKit renders as no title at all rather than as a blank slot.
func title(in window: WindowModel) -> String {
providers.lazy.compactMap { $0.title(in: window) }.first ?? ""
func title(in window: WindowModel, locale: Locale) -> String {
providers.lazy.compactMap { $0.title(in: window, locale: locale) }.first ?? ""
}
static func standard(hosts: HostLibraryModel) -> Self {
@@ -48,11 +50,12 @@ struct WindowTitleCatalog {
/// Vault pages are named after themselves — except Welcome, which is the app's
/// front door and so is named after the app.
struct VaultWindowTitleProvider: WindowTitleProviding {
func title(in window: WindowModel) -> String? {
func title(in window: WindowModel, locale: Locale) -> String? {
guard window.selectedDestination == .vault,
let page = window.vaultDestination.page
else { return nil }
return page == .welcome ? ApplicationIdentity.displayName : page.title
return page == .welcome
? ApplicationIdentity.displayName : page.title(locale: locale)
}
}
@@ -60,11 +63,13 @@ struct VaultWindowTitleProvider: WindowTitleProviding {
/// where you are; which chat you are in is the part worth reading from across
/// the desk.
struct AssistantWindowTitleProvider: WindowTitleProviding {
func title(in window: WindowModel) -> String? {
func title(in window: WindowModel, locale: Locale) -> String? {
guard window.selectedDestination == .assistant else { return nil }
if window.isAssistantArchivePresented { return String(localized: "Archived") }
if window.isAssistantArchivePresented {
return AppLocalization.string("Archived", locale: locale)
}
return window.assistantLibrary.selectedConversation?.derivedTitle
?? String(localized: "New Chat")
?? AppLocalization.string("New Chat", locale: locale)
}
}
@@ -72,8 +77,8 @@ struct AssistantWindowTitleProvider: WindowTitleProviding {
struct WorkspaceWindowTitleProvider: WindowTitleProviding {
let hosts: HostLibraryModel
func title(in window: WindowModel) -> String? {
func title(in window: WindowModel, locale: Locale) -> String? {
guard let workspace = window.selectedWorkspace else { return nil }
return workspace.title(in: hosts)
return workspace.title(in: hosts, locale: locale)
}
}
+8 -7
View File
@@ -287,9 +287,9 @@ final class Pane: Identifiable {
/// Resolved against the store so a renamed host renames its workspaces — unless
/// the user named this pane, in which case their word wins everywhere.
func title(in store: HostLibraryModel) -> String {
func title(in store: HostLibraryModel, locale: Locale = .current) -> String {
if let customTitle { return customTitle }
return numbered(derivedTitle(in: store))
return numbered(derivedTitle(in: store, locale: locale))
}
/// The number only ever decorates a *derived* name. A name the user typed is
@@ -299,16 +299,17 @@ final class Pane: Identifiable {
duplicateIndex > 0 ? "\(base) (\(duplicateIndex))" : base
}
private func derivedTitle(in store: HostLibraryModel) -> String {
private func derivedTitle(in store: HostLibraryModel, locale: Locale) -> String {
switch content {
case .sshSession(let sshSession):
sshSession.title
case .sftpSession(let sftpSession):
store.host(sftpSession.hostID)?.name ?? String(localized: "SFTP")
store.host(sftpSession.hostID)?.name
?? AppLocalization.string("SFTP", locale: locale)
case .serialSession(let serialSession):
serialSession.title
case .empty:
String(localized: "Empty")
AppLocalization.string("Empty", locale: locale)
}
}
@@ -316,14 +317,14 @@ final class Pane: Identifiable {
///
/// SFTP and SSH panes share Sessions, with their symbols distinguishing the
/// protocol. The row therefore keeps the host name concise and unprefixed.
func sidebarTitle(in store: HostLibraryModel) -> String {
func sidebarTitle(in store: HostLibraryModel, locale: Locale = .current) -> String {
if let customTitle { return customTitle }
if case .sftpSession(let sftpSession) = content,
let host = store.host(sftpSession.hostID)
{
return numbered(host.name)
}
return numbered(derivedTitle(in: store))
return numbered(derivedTitle(in: store, locale: locale))
}
func subtitle(in store: HostLibraryModel) -> String? {
+5 -3
View File
@@ -127,9 +127,11 @@ final class Workspace: Identifiable {
/// (see `WindowModel.rename`), and one thing has one name. It also means a
/// renamed workspace reads "Workspace" the moment it is split, since the name it
/// was given went to the pane rather than to the group.
func title(in store: HostLibraryModel) -> String {
if isSplit { return customTitle ?? String(localized: "Workspace") }
return focusedPane.title(in: store)
func title(in store: HostLibraryModel, locale: Locale = .current) -> String {
if isSplit {
return customTitle ?? AppLocalization.string("Workspace", locale: locale)
}
return focusedPane.title(in: store, locale: locale)
}
func subtitle(in store: HostLibraryModel) -> String? {
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
struct WorkspaceNavigatorView: View {
@Environment(AppModel.self) private var app
@Environment(WindowModel.self) private var window
@Environment(\.locale) private var locale
@AppStorage(SettingsKey.vaultSectionExpanded) private var vaultExpanded = true
@AppStorage(SettingsKey.workspaceSectionExpanded) private var workspaceExpanded = true
@@ -88,7 +89,7 @@ struct WorkspaceNavigatorView: View {
&& window.vaultDestination.page == page,
// The page's own New verbs, so adding a host does not
// require visiting Hosts first.
menu: { window.creationMenu(for: page) }
menu: { window.creationMenu(for: page, locale: locale) }
) {
window.dragging = nil
vaultExpanded = true
@@ -279,7 +280,7 @@ struct WorkspaceNavigatorView: View {
private func beginWorkspaceRename(_ workspace: Workspace) {
guard workspace.isRenameable else { return }
renamingWorkspaceID = workspace.id
workspaceRenameTitle = workspace.title(in: app.hostLibrary)
workspaceRenameTitle = workspace.title(in: app.hostLibrary, locale: locale)
}
private func commitWorkspaceRename(_ workspace: Workspace) {
@@ -334,6 +335,7 @@ private struct WorkspaceNavigatorRow: View {
@Environment(AppModel.self) private var app
@Environment(WindowModel.self) private var window
@Environment(\.controlActiveState) private var controlActiveState
@Environment(\.locale) private var locale
@State private var hovering = false
@FocusState private var titleFocused: Bool
@@ -355,7 +357,7 @@ private struct WorkspaceNavigatorRow: View {
.onExitCommand(perform: onCancelRename)
} else {
VStack(alignment: .leading, spacing: 1) {
Text(workspace.title(in: app.hostLibrary))
Text(workspace.title(in: app.hostLibrary, locale: locale))
.fontWeight(isSelected ? .semibold : .regular)
.lineLimit(1)
if let subtitle = workspace.subtitle(in: app.hostLibrary) {
@@ -387,9 +389,11 @@ private struct WorkspaceNavigatorRow: View {
window.dragging = .workspace(workspace.id)
return NSItemProvider(object: workspace.id.uuidString as NSString)
}
.help(Text(workspace.title(in: app.hostLibrary)))
.help(Text(workspace.title(in: app.hostLibrary, locale: locale)))
.accessibilityElement(children: .combine)
.accessibilityLabel(Text(workspace.title(in: app.hostLibrary)))
.accessibilityLabel(
Text(workspace.title(in: app.hostLibrary, locale: locale))
)
.accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton)
.onChange(of: isRenaming, initial: true) {
if isRenaming { DispatchQueue.main.async { titleFocused = true } }
@@ -424,11 +428,11 @@ private struct WorkspaceNavigatorRow: View {
var entries: [NativeMenuEntry] = []
if workspace.isRenameable {
entries.append(
NativeMenuEntry(title: String(localized: "Rename"), action: onBeginRename)
NativeMenuEntry(title: localized("Rename"), action: onBeginRename)
)
if namedTitle != nil {
entries.append(
NativeMenuEntry(title: String(localized: "Reset Name")) {
NativeMenuEntry(title: localized("Reset Name")) {
window.rename(workspace.id, to: "")
}
)
@@ -441,7 +445,7 @@ private struct WorkspaceNavigatorRow: View {
// detach from.
if workspace.isSplit {
entries.append(
NativeMenuEntry(title: String(localized: "Separate Panes")) {
NativeMenuEntry(title: localized("Separate Panes")) {
window.separatePanes(in: workspace.id)
}
)
@@ -450,19 +454,19 @@ private struct WorkspaceNavigatorRow: View {
if workspace.isCombinable {
if workspace.isSplit {
entries.append(
NativeMenuEntry(title: String(localized: "Duplicate in New Workspace")) {
NativeMenuEntry(title: localized("Duplicate in New Workspace")) {
act { window.duplicateSelectedInNewWorkspace() }
}
)
} else {
entries.append(
NativeMenuEntry(
title: String(localized: "Duplicate"),
title: localized("Duplicate"),
children: [
NativeMenuEntry(title: String(localized: "In Current Workspace")) {
NativeMenuEntry(title: localized("In Current Workspace")) {
act { window.duplicateSelected(axis: .horizontal) }
},
NativeMenuEntry(title: String(localized: "In New Workspace")) {
NativeMenuEntry(title: localized("In New Workspace")) {
act { window.duplicateSelectedInNewWorkspace() }
},
]
@@ -471,29 +475,29 @@ private struct WorkspaceNavigatorRow: View {
}
entries.append(.separator)
entries.append(
NativeMenuEntry(title: String(localized: "Split Right")) {
NativeMenuEntry(title: localized("Split Right")) {
act { window.splitSelected(axis: .horizontal) }
}
)
entries.append(
NativeMenuEntry(title: String(localized: "Split Down")) {
NativeMenuEntry(title: localized("Split Down")) {
act { window.splitSelected(axis: .vertical) }
}
)
entries.append(.separator)
}
entries.append(
NativeMenuEntry(title: String(localized: "Close Workspace")) {
NativeMenuEntry(title: localized("Close Workspace")) {
window.close(workspace.id)
}
)
entries.append(
NativeMenuEntry(title: String(localized: "Close Other Workspaces")) {
NativeMenuEntry(title: localized("Close Other Workspaces")) {
window.closeOthers(than: workspace.id)
}
)
entries.append(
NativeMenuEntry(title: String(localized: "Close All Workspaces")) {
NativeMenuEntry(title: localized("Close All Workspaces")) {
window.closeAll()
}
)
@@ -504,6 +508,10 @@ private struct WorkspaceNavigatorRow: View {
workspace.isSplit ? workspace.customTitle : workspace.focusedPane.customTitle
}
private func localized(_ key: String.LocalizationValue) -> String {
AppLocalization.string(key, locale: locale)
}
private func act(_ action: () -> Void) {
window.select(workspace.id)
action()
+4 -4
View File
@@ -638,7 +638,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld选中"
"value" : "已选择%lld项"
}
}
}
@@ -8225,7 +8225,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "固定"
"value" : "置顶"
}
}
}
@@ -8245,7 +8245,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "绑定"
"value" : "置顶"
}
}
}
@@ -13652,7 +13652,7 @@
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "撤销"
"value" : "取消置顶"
}
}
}
+30 -14
View File
@@ -464,6 +464,7 @@ struct WindowToolbarSearchSlot: View {
/// Welcome and on a workspace that has no searchable pane in front.
struct WindowToolbarFindField: View {
@Environment(WindowModel.self) private var window
@Environment(\.locale) private var locale
@State private var searchFieldOwner = NativeSearchFieldOwner()
var body: some View {
@@ -471,7 +472,7 @@ struct WindowToolbarFindField: View {
if let context = window.findContext() {
NativeSearchField(
text: context.query,
placeholder: String(localized: "Search"),
placeholder: AppLocalization.string("Search", locale: locale),
owner: searchFieldOwner,
focusRequest: context.focusRequest,
maintainsKeyboardFocus: window.isToolbarFindFocused,
@@ -481,8 +482,10 @@ struct WindowToolbarFindField: View {
onFocusChange: { window.isToolbarFindFocused = $0 }
)
.frame(width: Metrics.Toolbar.searchFieldWidth)
.help(Text(String(localized: "Search")))
.accessibilityLabel(Text(String(localized: "Search")))
.help(Text(AppLocalization.string("Search", locale: locale)))
.accessibilityLabel(
Text(AppLocalization.string("Search", locale: locale))
)
} else {
// Zero width, full height. A toolbar with nothing in it is
// shorter than one holding a search field, and the titlebar
@@ -507,19 +510,26 @@ struct WindowToolbarFindField: View {
/// something the page in front cannot show.
struct WindowToolbarLeadingActions: View {
@Environment(WindowModel.self) private var window
@Environment(\.locale) private var locale
var body: some View {
let actions = window.creationActions
if actions.count == 1, let only = actions.first {
Button(action: only.perform) {
Label(only.title, systemImage: "square.and.pencil")
Label(
only.localizedTitle(locale: locale),
systemImage: "square.and.pencil"
)
}
.help(Text(only.title))
.help(Text(only.localizedTitle(locale: locale)))
} else if !actions.isEmpty {
Menu {
ForEach(actions) { action in
Button(action.title, action: action.perform)
Button(
action.localizedTitle(locale: locale),
action: action.perform
)
}
} label: {
Label("New", systemImage: "square.and.pencil")
@@ -535,17 +545,21 @@ struct WindowToolbarLeadingActions: View {
/// One thing the titlebar's New control can make.
struct WindowToolbarCreationAction: Identifiable {
let id: String
let title: LocalizedStringKey
/// The same string for AppKit, which takes a `String` rather than a key.
let localizedTitle: String
private let titleKey: String.LocalizationValue
let perform: () -> Void
init(id: String, title: String, perform: @escaping () -> Void) {
init(
id: String, title: String.LocalizationValue,
perform: @escaping () -> Void
) {
self.id = id
self.title = LocalizedStringKey(title)
self.localizedTitle = title
titleKey = title
self.perform = perform
}
func localizedTitle(locale: Locale) -> String {
AppLocalization.string(titleKey, locale: locale)
}
}
// MARK: - Resolving the context
@@ -614,9 +628,11 @@ extension WindowModel {
}
/// Those verbs as native menu entries, for the Navigator's own rows.
func creationMenu(for page: VaultPage) -> [NativeMenuEntry] {
func creationMenu(for page: VaultPage, locale: Locale) -> [NativeMenuEntry] {
creationActions(for: page).map { action in
NativeMenuEntry(title: action.localizedTitle, action: action.perform)
NativeMenuEntry(
title: action.localizedTitle(locale: locale), action: action.perform
)
}
}
+3
View File
@@ -103,6 +103,9 @@ struct AppLanguageTests {
"Version": "版本",
"Welcome": "欢迎",
"Snippets": "片段",
"Pin": "置顶",
"Pinned": "置顶",
"Unpin": "取消置顶",
"Approve for me": "帮我批准",
"Full access": "完全访问权限",
]
+14
View File
@@ -78,4 +78,18 @@ struct ManagedChooserTests {
#expect(HostChooserPurpose.batchStartup.allowsMultiple)
#expect(HostChooserPurpose.batchStartup.showsWorkspaceChoice)
}
@Test
func batchStartupStringsResolveUsingThePresentedLocale() {
let chinese = Locale(identifier: "zh-Hans-CN")
let selectedCount = 2
#expect(HostConnectionProtocol.serial.displayName(locale: chinese) == "串口")
#expect(AppLocalization.string("Batch Startup", locale: chinese) == "批量启动")
#expect(
AppLocalization.string("\(selectedCount) selected", locale: chinese)
== "已选择2项"
)
#expect(AppLocalization.string("Select All", locale: chinese) == "全部选择")
}
}
+43 -9
View File
@@ -143,6 +143,16 @@ struct WindowToolbarFindTests {
#expect(window.creationActions.map(\.id) == ["snippet", "package"])
}
@Test
func creationActionsResolveUsingThePresentedLocale() throws {
let window = WindowModel()
window.openPage(.hosts)
let host = try #require(window.creationActions.first { $0.id == "host" })
#expect(host.localizedTitle(locale: Locale(identifier: "en")) == "New Host")
#expect(host.localizedTitle(locale: Locale(identifier: "zh-Hans-CN")) == "新建主机")
}
/// Each Vault page keeps its own query, and keeps it across a page switch —
/// the field lives in the titlebar and outlives the page under it.
@Test
@@ -165,19 +175,22 @@ struct WindowToolbarFindTests {
func titlebarNamesTheContentInFront() {
let window = WindowModel()
let title = VaultWindowTitleProvider()
let english = Locale(identifier: "en")
let chinese = Locale(identifier: "zh-Hans-CN")
// Welcome is the app's front door, so it is named after the app.
window.openPage(.welcome)
#expect(title.title(in: window) == ApplicationIdentity.displayName)
#expect(title.title(in: window, locale: chinese) == ApplicationIdentity.displayName)
window.openPage(.hosts)
#expect(title.title(in: window) == VaultPage.hosts.title)
#expect(title.title(in: window, locale: english) == "Hosts")
#expect(title.title(in: window, locale: chinese) == "主机")
window.openPage(.keychain)
#expect(title.title(in: window) == VaultPage.keychain.title)
#expect(title.title(in: window, locale: chinese) == "钥匙串")
window.openPage(.snippets)
#expect(title.title(in: window) == VaultPage.snippets.title)
#expect(title.title(in: window, locale: chinese) == "片段")
}
/// Each provider answers only for its own destination, so the catalog can
@@ -187,8 +200,9 @@ struct WindowToolbarFindTests {
let window = WindowModel()
window.selectAssistant()
#expect(VaultWindowTitleProvider().title(in: window) == nil)
#expect(AssistantWindowTitleProvider().title(in: window) != nil)
let locale = Locale(identifier: "zh-Hans-CN")
#expect(VaultWindowTitleProvider().title(in: window, locale: locale) == nil)
#expect(AssistantWindowTitleProvider().title(in: window, locale: locale) != nil)
}
/// The Assistant names the conversation, not the destination.
@@ -200,15 +214,35 @@ struct WindowToolbarFindTests {
store.insert(conversation)
let window = WindowModel(assistantLibrary: AssistantLibraryModel(store: store))
let title = AssistantWindowTitleProvider()
let locale = Locale(identifier: "zh-Hans-CN")
window.selectAssistant()
#expect(title.title(in: window) == String(localized: "New Chat"))
#expect(title.title(in: window, locale: locale) == "新建对话")
window.selectAssistant(conversation.id)
#expect(title.title(in: window) == "Nginx rollout")
#expect(title.title(in: window, locale: locale) == "Nginx rollout")
window.showAssistantArchive()
#expect(title.title(in: window) == String(localized: "Archived"))
#expect(title.title(in: window, locale: locale) == "已归档")
}
@Test
func workspaceTitleLocalizesOnlyBuiltInFallbacks() {
let store = HostLibraryModel()
let locale = Locale(identifier: "zh-Hans-CN")
let first = Pane(.empty)
let second = Pane(.empty)
let workspace = Workspace(pane: first)
workspace.layout = .split(
.init(
axis: .horizontal, fraction: 0.5,
first: .leaf(first), second: .leaf(second)
)
)
#expect(workspace.title(in: store, locale: locale) == "工作区")
workspace.customTitle = "Production"
#expect(workspace.title(in: store, locale: locale) == "Production")
}
/// A tag stops existing when its last host loses it, so a filter pinned to