Fix explicit Chinese localization in window chrome
This commit is contained in:
1 parent
8086a9142f
commit
2ab1fdd5fb
14 files changed
+220
-96
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" : "取消置顶"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ struct AppLanguageTests {
|
||||
"Version": "版本",
|
||||
"Welcome": "欢迎",
|
||||
"Snippets": "片段",
|
||||
"Pin": "置顶",
|
||||
"Pinned": "置顶",
|
||||
"Unpin": "取消置顶",
|
||||
"Approve for me": "帮我批准",
|
||||
"Full access": "完全访问权限",
|
||||
]
|
||||
|
||||
@@ -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) == "全部选择")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user