feat: add Japanese localization with String Catalog (#819)
* Add i18n infrastructure with String Catalog and Japanese translations Introduce String Catalog (.xcstrings) for localization support: - Localizable.xcstrings: 195 UI string entries with en and ja translations - InfoPlist.xcstrings: Info.plist strings (microphone usage, Finder menu items) - project.pbxproj: add xcstrings to build phase and ja to knownRegions * Replace hardcoded UI strings with String(localized:defaultValue:) Migrate all user-facing strings across 11 source files to use String(localized:defaultValue:) API (macOS 13+). Each string references a key in Localizable.xcstrings, with the English text preserved as defaultValue for fallback. Files modified: - KeyboardShortcutSettings: 28 shortcut labels - SocketControlSettings: mode names and descriptions - TabManager: placement labels, color names, close dialogs - BrowserPanel/BrowserPanelView: error pages, context menus, tooltips - UpdateViewModel/UpdatePopoverView/UpdatePill: update UI states - NotificationsPage: notification panel labels - SurfaceSearchOverlay: search bar placeholder and tooltips - AppDelegate: menus, dialogs, command palette items * Fix localization gaps from review feedback Address review comments from CodeRabbit, Greptile, and Cubic Dev AI: - Use interpolated String(localized:) instead of concatenation for version/progress strings in UpdateViewModel - Localize remaining hardcoded strings in AppDelegate: window labels, rename dialog, status menu items, unread notification count - Localize insecure HTTP alert body in BrowserPanel - Add 12 new entries to Localizable.xcstrings with Japanese translations * Fix String(localized:defaultValue:) keys to use StaticString The localized: parameter requires StaticString when defaultValue: is used. Move string interpolation from the key to defaultValue only, and revert maxWidthText to plain strings since they are only used for layout width calculation. * Localize remaining UI strings across all source files Add String(localized:defaultValue:) to all user-facing strings in: - cmuxApp.swift: settings screen, menus, about panel, dialogs (~180 strings) - ContentView.swift: command palette, sidebar context menu, dialogs (~200 strings) - Workspace.swift: rename/move/close tab dialogs, tooltips (~20 strings) - UpdateTitlebarAccessory.swift: titlebar tooltips, notifications popover (~10 strings) - TerminalNotificationStore.swift: notification permission dialog (4 strings) - CmuxWebView.swift: browser context menu items (2 strings) - AppDelegate.swift: CLI install/uninstall alerts (6 strings) Add 418 new entries to Localizable.xcstrings with Japanese translations. Extract sidebar context menu into separate @ViewBuilder to fix Swift type-checker timeout in large body. Fix xcstrings format specifiers for interpolated strings (%lld, %@). Total: 624 localization entries covering the full UI. * Address review feedback: fix missing localizations and terminology - Localize javaScriptDialogTitle URL branch in BrowserPanel - Localize cantReach error message in BrowserPanel - Localize close other tabs dialog message in TabManager - Localize workspace accessibility label in ContentView - Fix unread notification singular/plural (split into two keys) - Fix insecure connection apostrophe inconsistency (unify to U+2019) - Rename socketControl.fullOpen.description to socketControl.allowAll.description - Remove dead code: renameTargetNoun function - Fix terminology inconsistencies in xcstrings: - Unify "Developer Tools" to デベロッパツール - Unify "Jump to Latest Unread" phrasing - Unify "Flash Focused Panel" terminology - Fix dialog.enableNotifications.notNow translation * fix: address remaining PR 819 review feedback * fix: use a single localized key for close-other-tabs * fix: avoid inflection markup in close-other-tabs message * Address review feedback: localize tooltip, fix subtitle concat, unify keys - Localize menubar tooltip unread count (hardcoded English -> localized) - Replace subtitle string concatenation anti-pattern with single localized keys containing interpolation placeholders - Unify workspace fallback key to workspace.displayName.fallback - Remove unused workspace.defaultName key from xcstrings - Add Japanese translations for new tooltip and subtitle keys
This commit is contained in:
parent
422c86e822
commit
2c330efb8a
20 changed files with 11643 additions and 789 deletions
|
|
@ -84,6 +84,8 @@
|
|||
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */; };
|
||||
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
|
||||
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */; };
|
||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
|
||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -215,7 +217,9 @@
|
|||
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateShortcutRoutingTests.swift; sourceTree = "<group>"; };
|
||||
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
|
||||
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketControlPasswordStoreTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
A5001030 /* Frameworks */ = {
|
||||
|
|
@ -261,6 +265,8 @@
|
|||
A5001100 /* Assets.xcassets in Resources */,
|
||||
84E00D47E4584162AE53BC8D /* xterm-ghostty in Resources */,
|
||||
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */,
|
||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */,
|
||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -385,6 +391,8 @@
|
|||
B2E7294509CC42FE9191870E /* xterm-ghostty */,
|
||||
A5002001 /* THIRD_PARTY_LICENSES.md */,
|
||||
C1ADE00001A1B2C3D4E5F719 /* claude */,
|
||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */,
|
||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -534,6 +542,7 @@
|
|||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
ja,
|
||||
);
|
||||
mainGroup = A5001040;
|
||||
packageReferences = (
|
||||
|
|
|
|||
57
Resources/InfoPlist.xcstrings
Normal file
57
Resources/InfoPlist.xcstrings
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"sourceLanguage" : "en",
|
||||
"version" : "1.0",
|
||||
"strings" : {
|
||||
"NSMicrophoneUsageDescription" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "A program running within cmux would like to use your microphone."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "cmux 内で実行中のプログラムがマイクの使用を求めています。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New $(PRODUCT_NAME) Workspace Here" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "New $(PRODUCT_NAME) Workspace Here"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ここに新規 $(PRODUCT_NAME) ワークスペースを作成"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"New $(PRODUCT_NAME) Window Here" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "New $(PRODUCT_NAME) Window Here"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ここに新規 $(PRODUCT_NAME) ウインドウを作成"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10733
Resources/Localizable.xcstrings
Normal file
10733
Resources/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -80,31 +80,31 @@ enum TerminalDirectoryOpenTarget: String, CaseIterable {
|
|||
var commandPaletteTitle: String {
|
||||
switch self {
|
||||
case .androidStudio:
|
||||
return "Open Current Directory in Android Studio"
|
||||
return String(localized: "menu.openInAndroidStudio", defaultValue: "Open Current Directory in Android Studio")
|
||||
case .antigravity:
|
||||
return "Open Current Directory in Antigravity"
|
||||
return String(localized: "menu.openInAntigravity", defaultValue: "Open Current Directory in Antigravity")
|
||||
case .cursor:
|
||||
return "Open Current Directory in Cursor"
|
||||
return String(localized: "menu.openInCursor", defaultValue: "Open Current Directory in Cursor")
|
||||
case .finder:
|
||||
return "Open Current Directory in Finder"
|
||||
return String(localized: "menu.openInFinder", defaultValue: "Open Current Directory in Finder")
|
||||
case .ghostty:
|
||||
return "Open Current Directory in Ghostty"
|
||||
return String(localized: "menu.openInGhostty", defaultValue: "Open Current Directory in Ghostty")
|
||||
case .iterm2:
|
||||
return "Open Current Directory in iTerm2"
|
||||
return String(localized: "menu.openInITerm2", defaultValue: "Open Current Directory in iTerm2")
|
||||
case .terminal:
|
||||
return "Open Current Directory in Terminal"
|
||||
return String(localized: "menu.openInTerminal", defaultValue: "Open Current Directory in Terminal")
|
||||
case .tower:
|
||||
return "Open Current Directory in Tower"
|
||||
return String(localized: "menu.openInTower", defaultValue: "Open Current Directory in Tower")
|
||||
case .vscode:
|
||||
return "Open Current Directory in VS Code (Inline)"
|
||||
return String(localized: "menu.openInVSCode", defaultValue: "Open Current Directory in VS Code (Inline)")
|
||||
case .warp:
|
||||
return "Open Current Directory in Warp"
|
||||
return String(localized: "menu.openInWarp", defaultValue: "Open Current Directory in Warp")
|
||||
case .windsurf:
|
||||
return "Open Current Directory in Windsurf"
|
||||
return String(localized: "menu.openInWindsurf", defaultValue: "Open Current Directory in Windsurf")
|
||||
case .xcode:
|
||||
return "Open Current Directory in Xcode"
|
||||
return String(localized: "menu.openInXcode", defaultValue: "Open Current Directory in Xcode")
|
||||
case .zed:
|
||||
return "Open Current Directory in Zed"
|
||||
return String(localized: "menu.openInZed", defaultValue: "Open Current Directory in Zed")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1461,7 +1461,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
private lazy var titlebarAccessoryController = UpdateTitlebarAccessoryController(viewModel: updateViewModel)
|
||||
private let windowDecorationsController = WindowDecorationsController()
|
||||
private var menuBarExtraController: MenuBarExtraController?
|
||||
private static let serviceErrorNoPath = NSString(string: "Could not load any folder path from the clipboard.")
|
||||
private static let serviceErrorNoPath = NSString(string: String(localized: "error.clipboardFolderPath", defaultValue: "Could not load any folder path from the clipboard."))
|
||||
private static let didInstallWindowKeyEquivalentSwizzle: Void = {
|
||||
let targetClass: AnyClass = NSWindow.self
|
||||
let originalSelector = #selector(NSWindow.performKeyEquivalent(with:))
|
||||
|
|
@ -3695,9 +3695,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
var labels: [UUID: String] = [:]
|
||||
for (index, summary) in orderedSummaries.enumerated() {
|
||||
if summary.windowId == referenceWindowId {
|
||||
labels[summary.windowId] = "Current Window"
|
||||
labels[summary.windowId] = String(localized: "menu.currentWindow", defaultValue: "Current Window")
|
||||
} else {
|
||||
labels[summary.windowId] = "Window \(index + 1)"
|
||||
let number = index + 1
|
||||
labels[summary.windowId] = String(localized: "menu.windowNumber", defaultValue: "Window \(number)")
|
||||
}
|
||||
}
|
||||
return labels
|
||||
|
|
@ -3705,7 +3706,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
|
||||
private func workspaceDisplayName(_ workspace: Workspace) -> String {
|
||||
let trimmed = workspace.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? "Workspace" : trimmed
|
||||
return trimmed.isEmpty ? String(localized: "workspace.displayName.fallback", defaultValue: "Workspace") : trimmed
|
||||
}
|
||||
|
||||
private func rollbackDetachedSurface(
|
||||
|
|
@ -4648,22 +4649,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
let installer = CmuxCLIPathInstaller()
|
||||
do {
|
||||
let outcome = try installer.install()
|
||||
var informativeText = """
|
||||
Created symlink:
|
||||
|
||||
\(outcome.destinationURL.path) -> \(outcome.sourceURL.path)
|
||||
"""
|
||||
var informativeText = String(localized: "cli.install.symlinkCreated", defaultValue: "Created symlink:\n\n\(outcome.destinationURL.path) -> \(outcome.sourceURL.path)")
|
||||
if outcome.usedAdministratorPrivileges {
|
||||
informativeText += "\n\nAdministrator privileges were required to write to /usr/local/bin."
|
||||
informativeText += "\n\n" + String(localized: "cli.install.adminRequired", defaultValue: "Administrator privileges were required to write to /usr/local/bin.")
|
||||
}
|
||||
presentCLIPathAlert(
|
||||
title: "cmux CLI Installed",
|
||||
title: String(localized: "cli.installed", defaultValue: "cmux CLI Installed"),
|
||||
informativeText: informativeText,
|
||||
style: .informational
|
||||
)
|
||||
} catch {
|
||||
presentCLIPathAlert(
|
||||
title: "Couldn't Install cmux CLI",
|
||||
title: String(localized: "cli.installFailed", defaultValue: "Couldn't Install cmux CLI"),
|
||||
informativeText: error.localizedDescription,
|
||||
style: .warning
|
||||
)
|
||||
|
|
@ -4675,20 +4672,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
do {
|
||||
let outcome = try installer.uninstall()
|
||||
let prefix = outcome.removedExistingEntry
|
||||
? "Removed \(outcome.destinationURL.path)."
|
||||
: "No cmux CLI symlink was found at \(outcome.destinationURL.path)."
|
||||
? String(localized: "cli.uninstall.removed", defaultValue: "Removed \(outcome.destinationURL.path).")
|
||||
: String(localized: "cli.uninstall.notFound", defaultValue: "No cmux CLI symlink was found at \(outcome.destinationURL.path).")
|
||||
var informativeText = prefix
|
||||
if outcome.usedAdministratorPrivileges {
|
||||
informativeText += "\n\nAdministrator privileges were required to modify /usr/local/bin."
|
||||
informativeText += "\n\n" + String(localized: "cli.uninstall.adminRequired", defaultValue: "Administrator privileges were required to modify /usr/local/bin.")
|
||||
}
|
||||
presentCLIPathAlert(
|
||||
title: "cmux CLI Uninstalled",
|
||||
title: String(localized: "cli.uninstalled", defaultValue: "cmux CLI Uninstalled"),
|
||||
informativeText: informativeText,
|
||||
style: .informational
|
||||
)
|
||||
} catch {
|
||||
presentCLIPathAlert(
|
||||
title: "Couldn't Uninstall cmux CLI",
|
||||
title: String(localized: "cli.uninstallFailed", defaultValue: "Couldn't Uninstall cmux CLI"),
|
||||
informativeText: error.localizedDescription,
|
||||
style: .warning
|
||||
)
|
||||
|
|
@ -4704,7 +4701,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
alert.alertStyle = style
|
||||
alert.messageText = title
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
|
||||
|
||||
if let window = NSApp.keyWindow ?? NSApp.mainWindow {
|
||||
alert.beginSheetModal(for: window, completionHandler: nil)
|
||||
|
|
@ -6003,12 +6000,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = "Quit cmux?"
|
||||
alert.informativeText = "This will close all windows and workspaces."
|
||||
alert.addButton(withTitle: "Quit")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.messageText = String(localized: "dialog.quitCmux.title", defaultValue: "Quit cmux?")
|
||||
alert.informativeText = String(localized: "dialog.quitCmux.message", defaultValue: "This will close all windows and workspaces.")
|
||||
alert.addButton(withTitle: String(localized: "dialog.quitCmux.quit", defaultValue: "Quit"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
alert.showsSuppressionButton = true
|
||||
alert.suppressionButton?.title = "Don't warn again for Cmd+Q"
|
||||
alert.suppressionButton?.title = String(localized: "dialog.dontWarnCmdQ", defaultValue: "Don't warn again for Cmd+Q")
|
||||
|
||||
let response = alert.runModal()
|
||||
if alert.suppressionButton?.state == .on {
|
||||
|
|
@ -6030,14 +6027,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Rename Workspace"
|
||||
alert.informativeText = "Enter a custom name for this workspace."
|
||||
alert.messageText = String(localized: "dialog.renameWorkspace.title", defaultValue: "Rename Workspace")
|
||||
alert.informativeText = String(localized: "dialog.renameWorkspace.message", defaultValue: "Enter a custom name for this workspace.")
|
||||
let input = NSTextField(string: tab.customTitle ?? tab.title)
|
||||
input.placeholderString = "Workspace name"
|
||||
input.placeholderString = String(localized: "dialog.renameWorkspace.placeholder", defaultValue: "Workspace name")
|
||||
input.frame = NSRect(x: 0, y: 0, width: 240, height: 22)
|
||||
alert.accessoryView = input
|
||||
alert.addButton(withTitle: "Rename")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "common.rename", defaultValue: "Rename"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
let alertWindow = alert.window
|
||||
alertWindow.initialFirstResponder = input
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -8083,17 +8080,17 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
|
|||
private var notificationsCancellable: AnyCancellable?
|
||||
private let buildHintTitle: String?
|
||||
|
||||
private let stateHintItem = NSMenuItem(title: "No unread notifications", action: nil, keyEquivalent: "")
|
||||
private let stateHintItem = NSMenuItem(title: String(localized: "statusMenu.noUnread", defaultValue: "No unread notifications"), action: nil, keyEquivalent: "")
|
||||
private let buildHintItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
||||
private let notificationListSeparator = NSMenuItem.separator()
|
||||
private let notificationSectionSeparator = NSMenuItem.separator()
|
||||
private let showNotificationsItem = NSMenuItem(title: "Show Notifications", action: nil, keyEquivalent: "")
|
||||
private let jumpToUnreadItem = NSMenuItem(title: "Jump to Latest Unread", action: nil, keyEquivalent: "")
|
||||
private let markAllReadItem = NSMenuItem(title: "Mark All Read", action: nil, keyEquivalent: "")
|
||||
private let clearAllItem = NSMenuItem(title: "Clear All", action: nil, keyEquivalent: "")
|
||||
private let checkForUpdatesItem = NSMenuItem(title: "Check for Updates…", action: nil, keyEquivalent: "")
|
||||
private let preferencesItem = NSMenuItem(title: "Preferences…", action: nil, keyEquivalent: "")
|
||||
private let quitItem = NSMenuItem(title: "Quit cmux", action: nil, keyEquivalent: "")
|
||||
private let showNotificationsItem = NSMenuItem(title: String(localized: "statusMenu.showNotifications", defaultValue: "Show Notifications"), action: nil, keyEquivalent: "")
|
||||
private let jumpToUnreadItem = NSMenuItem(title: String(localized: "statusMenu.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"), action: nil, keyEquivalent: "")
|
||||
private let markAllReadItem = NSMenuItem(title: String(localized: "statusMenu.markAllRead", defaultValue: "Mark All Read"), action: nil, keyEquivalent: "")
|
||||
private let clearAllItem = NSMenuItem(title: String(localized: "statusMenu.clearAll", defaultValue: "Clear All"), action: nil, keyEquivalent: "")
|
||||
private let checkForUpdatesItem = NSMenuItem(title: String(localized: "menu.checkForUpdates", defaultValue: "Check for Updates…"), action: nil, keyEquivalent: "")
|
||||
private let preferencesItem = NSMenuItem(title: String(localized: "menu.preferences", defaultValue: "Preferences…"), action: nil, keyEquivalent: "")
|
||||
private let quitItem = NSMenuItem(title: String(localized: "menu.quitCmux", defaultValue: "Quit cmux"), action: nil, keyEquivalent: "")
|
||||
|
||||
private var notificationItems: [NSMenuItem] = []
|
||||
private let maxInlineNotificationItems = 6
|
||||
|
|
@ -8222,7 +8219,9 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
|
|||
button.image = MenuBarIconRenderer.makeImage(unreadCount: displayedUnreadCount)
|
||||
button.toolTip = displayedUnreadCount == 0
|
||||
? "cmux"
|
||||
: "cmux: \(displayedUnreadCount) unread notification\(displayedUnreadCount == 1 ? "" : "s")"
|
||||
: displayedUnreadCount == 1
|
||||
? "cmux: " + String(localized: "statusMenu.tooltip.unread.one", defaultValue: "1 unread notification")
|
||||
: "cmux: " + String(localized: "statusMenu.tooltip.unread.other", defaultValue: "\(displayedUnreadCount) unread notifications")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8345,9 +8344,14 @@ enum NotificationMenuSnapshotBuilder {
|
|||
}
|
||||
|
||||
static func stateHintTitle(unreadCount: Int) -> String {
|
||||
unreadCount == 0
|
||||
? "No unread notifications"
|
||||
: "\(unreadCount) unread notification\(unreadCount == 1 ? "" : "s")"
|
||||
switch unreadCount {
|
||||
case 0:
|
||||
return String(localized: "statusMenu.noUnread", defaultValue: "No unread notifications")
|
||||
case 1:
|
||||
return String(localized: "statusMenu.unreadCount.one", defaultValue: "1 unread notification")
|
||||
default:
|
||||
return String(localized: "statusMenu.unreadCount.other", defaultValue: "\(unreadCount) unread notifications")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -74,7 +74,7 @@ struct SurfaceSearchOverlay: View {
|
|||
Image(systemName: "chevron.up")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.help("Next match (Return)")
|
||||
.help(String(localized: "search.nextMatch.help", defaultValue: "Next match (Return)"))
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
|
|
@ -85,7 +85,7 @@ struct SurfaceSearchOverlay: View {
|
|||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.help("Previous match (Shift+Return)")
|
||||
.help(String(localized: "search.previousMatch.help", defaultValue: "Previous match (Shift+Return)"))
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
|
|
@ -96,7 +96,7 @@ struct SurfaceSearchOverlay: View {
|
|||
Image(systemName: "xmark")
|
||||
}
|
||||
.buttonStyle(SearchButtonStyle())
|
||||
.help("Close (Esc)")
|
||||
.help(String(localized: "search.close.help", defaultValue: "Close (Esc)"))
|
||||
}
|
||||
.padding(8)
|
||||
.background(.background)
|
||||
|
|
@ -288,7 +288,7 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
|
|||
func makeNSView(context: Context) -> SearchNativeTextField {
|
||||
let field = SearchNativeTextField(frame: .zero)
|
||||
field.font = .systemFont(ofSize: NSFont.systemFontSize)
|
||||
field.placeholderString = "Search"
|
||||
field.placeholderString = String(localized: "search.placeholder", defaultValue: "Search")
|
||||
field.delegate = context.coordinator
|
||||
field.stringValue = text
|
||||
context.coordinator.parentField = field
|
||||
|
|
|
|||
|
|
@ -45,35 +45,35 @@ enum KeyboardShortcutSettings {
|
|||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .toggleSidebar: return "Toggle Sidebar"
|
||||
case .newTab: return "New Workspace"
|
||||
case .newWindow: return "New Window"
|
||||
case .closeWindow: return "Close Window"
|
||||
case .openFolder: return "Open Folder"
|
||||
case .showNotifications: return "Show Notifications"
|
||||
case .jumpToUnread: return "Jump to Latest Unread"
|
||||
case .triggerFlash: return "Flash Focused Panel"
|
||||
case .nextSurface: return "Next Surface"
|
||||
case .prevSurface: return "Previous Surface"
|
||||
case .nextSidebarTab: return "Next Workspace"
|
||||
case .prevSidebarTab: return "Previous Workspace"
|
||||
case .renameTab: return "Rename Tab"
|
||||
case .renameWorkspace: return "Rename Workspace"
|
||||
case .closeWorkspace: return "Close Workspace"
|
||||
case .newSurface: return "New Surface"
|
||||
case .toggleTerminalCopyMode: return "Toggle Terminal Copy Mode"
|
||||
case .focusLeft: return "Focus Pane Left"
|
||||
case .focusRight: return "Focus Pane Right"
|
||||
case .focusUp: return "Focus Pane Up"
|
||||
case .focusDown: return "Focus Pane Down"
|
||||
case .splitRight: return "Split Right"
|
||||
case .splitDown: return "Split Down"
|
||||
case .toggleSplitZoom: return "Toggle Pane Zoom"
|
||||
case .splitBrowserRight: return "Split Browser Right"
|
||||
case .splitBrowserDown: return "Split Browser Down"
|
||||
case .openBrowser: return "Open Browser"
|
||||
case .toggleBrowserDeveloperTools: return "Toggle Browser Developer Tools"
|
||||
case .showBrowserJavaScriptConsole: return "Show Browser JavaScript Console"
|
||||
case .toggleSidebar: return String(localized: "shortcut.toggleSidebar.label", defaultValue: "Toggle Sidebar")
|
||||
case .newTab: return String(localized: "shortcut.newWorkspace.label", defaultValue: "New Workspace")
|
||||
case .newWindow: return String(localized: "shortcut.newWindow.label", defaultValue: "New Window")
|
||||
case .closeWindow: return String(localized: "shortcut.closeWindow.label", defaultValue: "Close Window")
|
||||
case .openFolder: return String(localized: "shortcut.openFolder.label", defaultValue: "Open Folder")
|
||||
case .showNotifications: return String(localized: "shortcut.showNotifications.label", defaultValue: "Show Notifications")
|
||||
case .jumpToUnread: return String(localized: "shortcut.jumpToUnread.label", defaultValue: "Jump to Latest Unread")
|
||||
case .triggerFlash: return String(localized: "shortcut.flashFocusedPanel.label", defaultValue: "Flash Focused Panel")
|
||||
case .nextSurface: return String(localized: "shortcut.nextSurface.label", defaultValue: "Next Surface")
|
||||
case .prevSurface: return String(localized: "shortcut.previousSurface.label", defaultValue: "Previous Surface")
|
||||
case .nextSidebarTab: return String(localized: "shortcut.nextWorkspace.label", defaultValue: "Next Workspace")
|
||||
case .prevSidebarTab: return String(localized: "shortcut.previousWorkspace.label", defaultValue: "Previous Workspace")
|
||||
case .renameTab: return String(localized: "shortcut.renameTab.label", defaultValue: "Rename Tab")
|
||||
case .renameWorkspace: return String(localized: "shortcut.renameWorkspace.label", defaultValue: "Rename Workspace")
|
||||
case .closeWorkspace: return String(localized: "shortcut.closeWorkspace.label", defaultValue: "Close Workspace")
|
||||
case .newSurface: return String(localized: "shortcut.newSurface.label", defaultValue: "New Surface")
|
||||
case .toggleTerminalCopyMode: return String(localized: "shortcut.toggleTerminalCopyMode.label", defaultValue: "Toggle Terminal Copy Mode")
|
||||
case .focusLeft: return String(localized: "shortcut.focusPaneLeft.label", defaultValue: "Focus Pane Left")
|
||||
case .focusRight: return String(localized: "shortcut.focusPaneRight.label", defaultValue: "Focus Pane Right")
|
||||
case .focusUp: return String(localized: "shortcut.focusPaneUp.label", defaultValue: "Focus Pane Up")
|
||||
case .focusDown: return String(localized: "shortcut.focusPaneDown.label", defaultValue: "Focus Pane Down")
|
||||
case .splitRight: return String(localized: "shortcut.splitRight.label", defaultValue: "Split Right")
|
||||
case .splitDown: return String(localized: "shortcut.splitDown.label", defaultValue: "Split Down")
|
||||
case .toggleSplitZoom: return String(localized: "shortcut.togglePaneZoom.label", defaultValue: "Toggle Pane Zoom")
|
||||
case .splitBrowserRight: return String(localized: "shortcut.splitBrowserRight.label", defaultValue: "Split Browser Right")
|
||||
case .splitBrowserDown: return String(localized: "shortcut.splitBrowserDown.label", defaultValue: "Split Browser Down")
|
||||
case .openBrowser: return String(localized: "shortcut.openBrowser.label", defaultValue: "Open Browser")
|
||||
case .toggleBrowserDeveloperTools: return String(localized: "shortcut.toggleBrowserDevTools.label", defaultValue: "Toggle Browser Developer Tools")
|
||||
case .showBrowserJavaScriptConsole: return String(localized: "shortcut.showBrowserJSConsole.label", defaultValue: "Show Browser JavaScript Console")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +474,7 @@ private class ShortcutRecorderNSButton: NSButton {
|
|||
|
||||
func updateTitle() {
|
||||
if isRecording {
|
||||
title = "Press shortcut…"
|
||||
title = String(localized: "shortcut.pressShortcut.prompt", defaultValue: "Press shortcut…")
|
||||
} else {
|
||||
title = shortcut.displayString
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ struct NotificationsPage: View {
|
|||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Text("Notifications")
|
||||
Text(String(localized: "notifications.title", defaultValue: "Notifications"))
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ struct NotificationsPage: View {
|
|||
if !notificationStore.notifications.isEmpty {
|
||||
jumpToUnreadButton
|
||||
|
||||
Button("Clear All") {
|
||||
Button(String(localized: "notifications.clearAll", defaultValue: "Clear All")) {
|
||||
notificationStore.clearAll()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -91,9 +91,9 @@ struct NotificationsPage: View {
|
|||
Image(systemName: "bell.slash")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.secondary)
|
||||
Text("No notifications yet")
|
||||
Text(String(localized: "notifications.empty.title", defaultValue: "No notifications yet"))
|
||||
.font(.headline)
|
||||
Text("Desktop notifications will appear here for quick review.")
|
||||
Text(String(localized: "notifications.empty.description", defaultValue: "Desktop notifications will appear here for quick review."))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
@ -107,25 +107,25 @@ struct NotificationsPage: View {
|
|||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Jump to Latest Unread")
|
||||
Text(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))
|
||||
ShortcutAnnotation(text: jumpToUnreadShortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.keyboardShortcut(key, modifiers: jumpToUnreadShortcut.eventModifiers)
|
||||
.help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip("Jump to Latest Unread"))
|
||||
.help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread")))
|
||||
.disabled(!hasUnreadNotifications)
|
||||
} else {
|
||||
Button(action: {
|
||||
AppDelegate.shared?.jumpToLatestUnread()
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Jump to Latest Unread")
|
||||
Text(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread"))
|
||||
ShortcutAnnotation(text: jumpToUnreadShortcut.displayString)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip("Jump to Latest Unread"))
|
||||
.help(KeyboardShortcutSettings.Action.jumpToUnread.tooltip(String(localized: "notifications.jumpToLatestUnread", defaultValue: "Jump to Latest Unread")))
|
||||
.disabled(!hasUnreadNotifications)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,11 +125,11 @@ enum BrowserThemeMode: String, CaseIterable, Identifiable {
|
|||
var displayName: String {
|
||||
switch self {
|
||||
case .system:
|
||||
return "System"
|
||||
return String(localized: "theme.system", defaultValue: "System")
|
||||
case .light:
|
||||
return "Light"
|
||||
return String(localized: "theme.light", defaultValue: "Light")
|
||||
case .dark:
|
||||
return "Dark"
|
||||
return String(localized: "theme.dark", defaultValue: "Dark")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1474,7 +1474,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
if let url = currentURL {
|
||||
return url.host ?? url.absoluteString
|
||||
}
|
||||
return "New tab"
|
||||
return String(localized: "browser.newTab", defaultValue: "New tab")
|
||||
}
|
||||
|
||||
var displayIcon: String? {
|
||||
|
|
@ -2074,17 +2074,13 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
|
||||
let alert = insecureHTTPAlertFactory()
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = "Connection isn't secure"
|
||||
alert.informativeText = """
|
||||
\(host) uses plain HTTP, so traffic can be read or modified on the network.
|
||||
|
||||
Open this URL in your default browser, or proceed in cmux.
|
||||
"""
|
||||
alert.addButton(withTitle: "Open in Default Browser")
|
||||
alert.addButton(withTitle: "Proceed in cmux")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.messageText = String(localized: "browser.error.insecure.title", defaultValue: "Connection isn\u{2019}t secure")
|
||||
alert.informativeText = String(localized: "browser.error.insecure.message", defaultValue: "\(host) uses plain HTTP, so traffic can be read or modified on the network.\n\nOpen this URL in your default browser, or proceed in cmux.")
|
||||
alert.addButton(withTitle: String(localized: "browser.openInDefaultBrowser", defaultValue: "Open in Default Browser"))
|
||||
alert.addButton(withTitle: String(localized: "browser.proceedInCmux", defaultValue: "Proceed in cmux"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
alert.showsSuppressionButton = true
|
||||
alert.suppressionButton?.title = "Always allow this host in cmux"
|
||||
alert.suppressionButton?.title = String(localized: "browser.alwaysAllowHost", defaultValue: "Always allow this host in cmux")
|
||||
|
||||
let handleResponse: (NSApplication.ModalResponse) -> Void = { [weak self, weak alert] response in
|
||||
self?.handleInsecureHTTPAlertResponse(
|
||||
|
|
@ -3098,29 +3094,40 @@ private class BrowserNavigationDelegate: NSObject, WKNavigationDelegate {
|
|||
case (NSURLErrorDomain, NSURLErrorCannotConnectToHost),
|
||||
(NSURLErrorDomain, NSURLErrorCannotFindHost),
|
||||
(NSURLErrorDomain, NSURLErrorTimedOut):
|
||||
title = "Can\u{2019}t reach this page"
|
||||
message = "\(failedURL.isEmpty ? "The site" : failedURL) refused to connect. Check that a server is running on this address."
|
||||
title = String(localized: "browser.error.cantReach.title", defaultValue: "Can\u{2019}t reach this page")
|
||||
if failedURL.isEmpty {
|
||||
message = String(localized: "browser.error.cantReach.messageSite", defaultValue: "The site refused to connect. Check that a server is running on this address.")
|
||||
} else {
|
||||
message = String(localized: "browser.error.cantReach.messageURL", defaultValue: "\(failedURL) refused to connect. Check that a server is running on this address.")
|
||||
}
|
||||
case (NSURLErrorDomain, NSURLErrorNotConnectedToInternet),
|
||||
(NSURLErrorDomain, NSURLErrorNetworkConnectionLost):
|
||||
title = "No internet connection"
|
||||
message = "Check your network connection and try again."
|
||||
title = String(localized: "browser.error.noInternet", defaultValue: "No internet connection")
|
||||
message = String(localized: "browser.error.checkNetwork", defaultValue: "Check your network connection and try again.")
|
||||
case (NSURLErrorDomain, NSURLErrorSecureConnectionFailed),
|
||||
(NSURLErrorDomain, NSURLErrorServerCertificateUntrusted),
|
||||
(NSURLErrorDomain, NSURLErrorServerCertificateHasUnknownRoot),
|
||||
(NSURLErrorDomain, NSURLErrorServerCertificateHasBadDate),
|
||||
(NSURLErrorDomain, NSURLErrorServerCertificateNotYetValid):
|
||||
title = "Connection isn\u{2019}t secure"
|
||||
message = "The certificate for this site is invalid."
|
||||
title = String(localized: "browser.error.insecure.title", defaultValue: "Connection isn\u{2019}t secure")
|
||||
message = String(localized: "browser.error.invalidCertificate", defaultValue: "The certificate for this site is invalid.")
|
||||
default:
|
||||
title = "Can\u{2019}t open this page"
|
||||
title = String(localized: "browser.error.cantOpen.title", defaultValue: "Can\u{2019}t open this page")
|
||||
message = error.localizedDescription
|
||||
}
|
||||
|
||||
let escapedURL = failedURL
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: "\"", with: """)
|
||||
let escapeHTML: (String) -> String = { value in
|
||||
value
|
||||
.replacingOccurrences(of: "&", with: "&")
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
.replacingOccurrences(of: "\"", with: """)
|
||||
}
|
||||
|
||||
let escapedTitle = escapeHTML(title)
|
||||
let escapedMessage = escapeHTML(message)
|
||||
let escapedURL = escapeHTML(failedURL)
|
||||
let escapedReloadLabel = escapeHTML(String(localized: "browser.error.reload", defaultValue: "Reload"))
|
||||
|
||||
let html = """
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -3156,10 +3163,10 @@ private class BrowserNavigationDelegate: NSObject, WKNavigationDelegate {
|
|||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>\(title)</h1>
|
||||
<p>\(message)</p>
|
||||
<h1>\(escapedTitle)</h1>
|
||||
<p>\(escapedMessage)</p>
|
||||
<div class="url">\(escapedURL)</div>
|
||||
<button onclick="location.reload()">Reload</button>
|
||||
<button onclick="location.reload()">\(escapedReloadLabel)</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3334,9 +3341,9 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
|
|||
|
||||
private func javaScriptDialogTitle(for webView: WKWebView) -> String {
|
||||
if let absolute = webView.url?.absoluteString, !absolute.isEmpty {
|
||||
return "The page at \(absolute) says:"
|
||||
return String(localized: "browser.dialog.pageSaysAt", defaultValue: "The page at \(absolute) says:")
|
||||
}
|
||||
return "This page says:"
|
||||
return String(localized: "browser.dialog.pageSays", defaultValue: "This page says:")
|
||||
}
|
||||
|
||||
private func presentDialog(
|
||||
|
|
@ -3429,7 +3436,7 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
|
|||
alert.alertStyle = .informational
|
||||
alert.messageText = javaScriptDialogTitle(for: webView)
|
||||
alert.informativeText = message
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
|
||||
presentDialog(alert, for: webView) { _ in completionHandler() }
|
||||
}
|
||||
|
||||
|
|
@ -3443,8 +3450,8 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
|
|||
alert.alertStyle = .informational
|
||||
alert.messageText = javaScriptDialogTitle(for: webView)
|
||||
alert.informativeText = message
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
presentDialog(alert, for: webView) { response in
|
||||
completionHandler(response == .alertFirstButtonReturn)
|
||||
}
|
||||
|
|
@ -3461,8 +3468,8 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
|
|||
alert.alertStyle = .informational
|
||||
alert.messageText = javaScriptDialogTitle(for: webView)
|
||||
alert.informativeText = prompt
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
|
||||
let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 320, height: 24))
|
||||
field.stringValue = defaultText ?? ""
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@ struct BrowserPanelView: View {
|
|||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.disabled(!panel.canGoBack)
|
||||
.opacity(panel.canGoBack ? 1.0 : 0.4)
|
||||
.help("Go Back")
|
||||
.help(String(localized: "browser.goBack", defaultValue: "Go Back"))
|
||||
|
||||
Button(action: {
|
||||
#if DEBUG
|
||||
|
|
@ -520,7 +520,7 @@ struct BrowserPanelView: View {
|
|||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.disabled(!panel.canGoForward)
|
||||
.opacity(panel.canGoForward ? 1.0 : 0.4)
|
||||
.help("Go Forward")
|
||||
.help(String(localized: "browser.goForward", defaultValue: "Go Forward"))
|
||||
|
||||
Button(action: {
|
||||
if panel.isLoading {
|
||||
|
|
@ -541,18 +541,18 @@ struct BrowserPanelView: View {
|
|||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.help(panel.isLoading ? "Stop" : "Reload")
|
||||
.help(panel.isLoading ? String(localized: "browser.stop", defaultValue: "Stop") : String(localized: "browser.reload", defaultValue: "Reload"))
|
||||
|
||||
if panel.isDownloading {
|
||||
HStack(spacing: 4) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("Downloading...")
|
||||
Text(String(localized: "browser.downloading", defaultValue: "Downloading..."))
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.help("Download in progress")
|
||||
.help(String(localized: "browser.downloadInProgress", defaultValue: "Download in progress"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -570,7 +570,7 @@ struct BrowserPanelView: View {
|
|||
}
|
||||
.buttonStyle(OmnibarAddressButtonStyle())
|
||||
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
|
||||
.help(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.tooltip("Toggle Developer Tools"))
|
||||
.help(KeyboardShortcutSettings.Action.toggleBrowserDeveloperTools.tooltip(String(localized: "browser.toggleDevTools", defaultValue: "Toggle Developer Tools")))
|
||||
.accessibilityIdentifier("BrowserToggleDevToolsButton")
|
||||
}
|
||||
|
||||
|
|
@ -651,7 +651,7 @@ struct BrowserPanelView: View {
|
|||
),
|
||||
isFocused: $addressBarFocused,
|
||||
inlineCompletion: inlineCompletion,
|
||||
placeholder: "Search or enter URL",
|
||||
placeholder: String(localized: "browser.addressBar.placeholder", defaultValue: "Search or enter URL"),
|
||||
onTap: {
|
||||
handleOmnibarTap()
|
||||
},
|
||||
|
|
@ -2146,7 +2146,7 @@ struct OmnibarSuggestion: Identifiable, Hashable {
|
|||
var trailingBadgeText: String? {
|
||||
switch kind {
|
||||
case .switchToTab:
|
||||
return "Switch to tab"
|
||||
return String(localized: "browser.switchToTab", defaultValue: "Switch to tab")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2983,7 +2983,7 @@ private struct OmnibarSuggestionsView: View {
|
|||
.accessibilityElement(children: .contain)
|
||||
.accessibilityRespondsToUserInteraction(true)
|
||||
.accessibilityIdentifier("BrowserOmnibarSuggestions")
|
||||
.accessibilityLabel("Address bar suggestions")
|
||||
.accessibilityLabel(String(localized: "browser.addressBarSuggestions", defaultValue: "Address bar suggestions"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1133,7 +1133,7 @@ final class CmuxWebView: WKWebView {
|
|||
debugLogContextMenuDownloadCandidate(item, index: index)
|
||||
if !hasDefaultBrowserOpenLinkItem,
|
||||
(item.action == #selector(contextMenuOpenLinkInDefaultBrowser(_:))
|
||||
|| item.title == "Open Link in Default Browser") {
|
||||
|| item.title == String(localized: "browser.contextMenu.openLinkInDefaultBrowser", defaultValue: "Open Link in Default Browser")) {
|
||||
hasDefaultBrowserOpenLinkItem = true
|
||||
}
|
||||
|
||||
|
|
@ -1148,7 +1148,7 @@ final class CmuxWebView: WKWebView {
|
|||
// by opening the link as a new surface in the same pane.
|
||||
if item.identifier?.rawValue == "WKMenuItemIdentifierOpenLinkInNewWindow"
|
||||
|| item.title.contains("Open Link in New Window") {
|
||||
item.title = "Open Link in New Tab"
|
||||
item.title = String(localized: "browser.contextMenu.openLinkInNewTab", defaultValue: "Open Link in New Tab")
|
||||
}
|
||||
|
||||
if isDownloadImageMenuItem(item) {
|
||||
|
|
@ -1188,7 +1188,7 @@ final class CmuxWebView: WKWebView {
|
|||
|
||||
if let openLinkInsertionIndex, !hasDefaultBrowserOpenLinkItem {
|
||||
let item = NSMenuItem(
|
||||
title: "Open Link in Default Browser",
|
||||
title: String(localized: "browser.contextMenu.openLinkInDefaultBrowser", defaultValue: "Open Link in Default Browser"),
|
||||
action: #selector(contextMenuOpenLinkInDefaultBrowser(_:)),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,30 +18,30 @@ enum SocketControlMode: String, CaseIterable, Identifiable {
|
|||
var displayName: String {
|
||||
switch self {
|
||||
case .off:
|
||||
return "Off"
|
||||
return String(localized: "socketControl.off.name", defaultValue: "Off")
|
||||
case .cmuxOnly:
|
||||
return "cmux processes only"
|
||||
return String(localized: "socketControl.cmuxOnly.name", defaultValue: "cmux processes only")
|
||||
case .automation:
|
||||
return "Automation mode"
|
||||
return String(localized: "socketControl.automation.name", defaultValue: "Automation mode")
|
||||
case .password:
|
||||
return "Password mode"
|
||||
return String(localized: "socketControl.password.name", defaultValue: "Password mode")
|
||||
case .allowAll:
|
||||
return "Full open access"
|
||||
return String(localized: "socketControl.allowAll.name", defaultValue: "Full open access")
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .off:
|
||||
return "Disable the local control socket."
|
||||
return String(localized: "socketControl.off.description", defaultValue: "Disable the local control socket.")
|
||||
case .cmuxOnly:
|
||||
return "Only processes started inside cmux terminals can send commands."
|
||||
return String(localized: "socketControl.cmuxOnly.description", defaultValue: "Only processes started inside cmux terminals can send commands.")
|
||||
case .automation:
|
||||
return "Allow external local automation clients from this macOS user (no ancestry check)."
|
||||
return String(localized: "socketControl.automation.description", defaultValue: "Allow external local automation clients from this macOS user (no ancestry check).")
|
||||
case .password:
|
||||
return "Require socket authentication with a password stored in a local file."
|
||||
return String(localized: "socketControl.password.description", defaultValue: "Require socket authentication with a password stored in a local file.")
|
||||
case .allowAll:
|
||||
return "Allow any local process and user to connect with no auth. Unsafe."
|
||||
return String(localized: "socketControl.allowAll.description", defaultValue: "Allow any local process and user to connect with no auth. Unsafe.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ enum SocketControlPasswordStore {
|
|||
throw NSError(
|
||||
domain: NSCocoaErrorDomain,
|
||||
code: NSFileNoSuchFileError,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to resolve socket password file path."]
|
||||
userInfo: [NSLocalizedDescriptionKey: String(localized: "socketControl.error.passwordFilePath", defaultValue: "Unable to resolve socket password file path.")]
|
||||
)
|
||||
}
|
||||
let directory = fileURL.deletingLastPathComponent()
|
||||
|
|
|
|||
|
|
@ -19,22 +19,22 @@ enum NewWorkspacePlacement: String, CaseIterable, Identifiable {
|
|||
var displayName: String {
|
||||
switch self {
|
||||
case .top:
|
||||
return "Top"
|
||||
return String(localized: "workspace.placement.top", defaultValue: "Top")
|
||||
case .afterCurrent:
|
||||
return "After current"
|
||||
return String(localized: "workspace.placement.afterCurrent", defaultValue: "After current")
|
||||
case .end:
|
||||
return "End"
|
||||
return String(localized: "workspace.placement.end", defaultValue: "End")
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .top:
|
||||
return "Insert new workspaces at the top of the list."
|
||||
return String(localized: "workspace.placement.top.description", defaultValue: "Insert new workspaces at the top of the list.")
|
||||
case .afterCurrent:
|
||||
return "Insert new workspaces directly after the active workspace."
|
||||
return String(localized: "workspace.placement.afterCurrent.description", defaultValue: "Insert new workspaces directly after the active workspace.")
|
||||
case .end:
|
||||
return "Append new workspaces to the bottom of the list."
|
||||
return String(localized: "workspace.placement.end.description", defaultValue: "Append new workspaces to the bottom of the list.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,9 +72,9 @@ enum SidebarActiveTabIndicatorStyle: String, CaseIterable, Identifiable {
|
|||
var displayName: String {
|
||||
switch self {
|
||||
case .leftRail:
|
||||
return "Left Rail"
|
||||
return String(localized: "sidebar.indicator.leftRail", defaultValue: "Left Rail")
|
||||
case .solidFill:
|
||||
return "Solid Fill"
|
||||
return String(localized: "sidebar.indicator.solidFill", defaultValue: "Solid Fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1078,9 +1078,13 @@ class TabManager: ObservableObject {
|
|||
|
||||
let count = plan.panelIds.count
|
||||
let titleLines = plan.titles.map { "• \($0)" }.joined(separator: "\n")
|
||||
let message = "This is about to close \(count) tab\(count == 1 ? "" : "s") in this pane:\n\(titleLines)"
|
||||
let message = if count == 1 {
|
||||
String(localized: "dialog.closeOtherTabs.message.one", defaultValue: "This will close 1 tab in this pane:\n\(titleLines)")
|
||||
} else {
|
||||
String(localized: "dialog.closeOtherTabs.message.other", defaultValue: "This will close \(count) tabs in this pane:\n\(titleLines)")
|
||||
}
|
||||
guard confirmClose(
|
||||
title: "Close other tabs?",
|
||||
title: String(localized: "dialog.closeOtherTabs.title", defaultValue: "Close other tabs?"),
|
||||
message: message,
|
||||
acceptCmdD: false
|
||||
) else { return }
|
||||
|
|
@ -1120,8 +1124,8 @@ class TabManager: ObservableObject {
|
|||
alert.messageText = title
|
||||
alert.informativeText = message
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "Close")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "common.close", defaultValue: "Close"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
|
||||
// macOS convention: Cmd+D = confirm destructive close (e.g. "Don't Save").
|
||||
// We only opt into this for the "close last workspace => close window" path to avoid
|
||||
|
|
@ -1182,15 +1186,15 @@ class TabManager: ObservableObject {
|
|||
if let collapsed, !collapsed.isEmpty {
|
||||
return collapsed
|
||||
}
|
||||
return "Untitled Tab"
|
||||
return String(localized: "tab.untitled", defaultValue: "Untitled Tab")
|
||||
}
|
||||
|
||||
private func closeWorkspaceIfRunningProcess(_ workspace: Workspace) {
|
||||
let willCloseWindow = tabs.count <= 1
|
||||
if workspaceNeedsConfirmClose(workspace),
|
||||
!confirmClose(
|
||||
title: "Close workspace?",
|
||||
message: "This will close the workspace and all of its panels.",
|
||||
title: String(localized: "dialog.closeWorkspace.title", defaultValue: "Close workspace?"),
|
||||
message: String(localized: "dialog.closeWorkspace.message", defaultValue: "This will close the workspace and all of its panels."),
|
||||
acceptCmdD: willCloseWindow
|
||||
) {
|
||||
return
|
||||
|
|
@ -1231,8 +1235,8 @@ class TabManager: ObservableObject {
|
|||
let needsConfirm = workspaceNeedsConfirmClose(tab)
|
||||
if needsConfirm {
|
||||
let message = willCloseWindow
|
||||
? "This will close the last tab and close the window."
|
||||
: "This will close the last tab and close its workspace."
|
||||
? String(localized: "dialog.closeLastTabWindow.message", defaultValue: "This will close the last tab and close the window.")
|
||||
: String(localized: "dialog.closeLastTabWorkspace.message", defaultValue: "This will close the last tab and close its workspace.")
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"surface.close.shortcut.confirm tab=\(tab.id.uuidString.prefix(5)) " +
|
||||
|
|
@ -1240,7 +1244,7 @@ class TabManager: ObservableObject {
|
|||
)
|
||||
#endif
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
|
||||
message: message,
|
||||
acceptCmdD: willCloseWindow
|
||||
) else {
|
||||
|
|
@ -1272,8 +1276,8 @@ class TabManager: ObservableObject {
|
|||
)
|
||||
#endif
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
message: "This will close the current tab.",
|
||||
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
|
||||
message: String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab."),
|
||||
acceptCmdD: false
|
||||
) else {
|
||||
#if DEBUG
|
||||
|
|
@ -1311,8 +1315,8 @@ class TabManager: ObservableObject {
|
|||
if let terminalPanel = tab.terminalPanel(for: surfaceId),
|
||||
terminalPanel.needsConfirmClose() {
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
message: "This will close the current tab.",
|
||||
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
|
||||
message: String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab."),
|
||||
acceptCmdD: false
|
||||
) else { return }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -545,10 +545,10 @@ final class TerminalNotificationStore: ObservableObject {
|
|||
}
|
||||
|
||||
let alert = notificationSettingsAlertFactory()
|
||||
alert.messageText = "Enable Notifications for cmux"
|
||||
alert.informativeText = "Notifications are disabled for cmux. Enable them in System Settings to see alerts."
|
||||
alert.addButton(withTitle: "Open Settings")
|
||||
alert.addButton(withTitle: "Not Now")
|
||||
alert.messageText = String(localized: "dialog.enableNotifications.title", defaultValue: "Enable Notifications for cmux")
|
||||
alert.informativeText = String(localized: "dialog.enableNotifications.message", defaultValue: "Notifications are disabled for cmux. Enable them in System Settings to see alerts.")
|
||||
alert.addButton(withTitle: String(localized: "dialog.enableNotifications.openSettings", defaultValue: "Open Settings"))
|
||||
alert.addButton(withTitle: String(localized: "dialog.enableNotifications.notNow", defaultValue: "Not Now"))
|
||||
alert.beginSheetModal(for: window) { [weak self] response in
|
||||
guard response == .alertFirstButtonReturn,
|
||||
let url = URL(string: "x-apple.systempreferences:com.apple.preference.notifications") else {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ struct InstallUpdateMenuItem: View {
|
|||
|
||||
var body: some View {
|
||||
if model.state.isInstallable {
|
||||
Button("Install Update and Relaunch") {
|
||||
Button(String(localized: "update.installAndRelaunch", defaultValue: "Install Update and Relaunch")) {
|
||||
model.state.confirm()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,17 +49,17 @@ fileprivate struct PermissionRequestView: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Enable automatic updates?")
|
||||
Text(String(localized: "update.popover.enableAutoUpdates", defaultValue: "Enable automatic updates?"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("cmux can automatically check for updates in the background.")
|
||||
Text(String(localized: "update.popover.autoUpdatesDescription", defaultValue: "cmux can automatically check for updates in the background."))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Not Now") {
|
||||
Button(String(localized: "common.notNow", defaultValue: "Not Now")) {
|
||||
request.reply(SUUpdatePermissionResponse(
|
||||
automaticUpdateChecks: false,
|
||||
sendSystemProfile: false))
|
||||
|
|
@ -69,7 +69,7 @@ fileprivate struct PermissionRequestView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button("Allow") {
|
||||
Button(String(localized: "common.allow", defaultValue: "Allow")) {
|
||||
request.reply(SUUpdatePermissionResponse(
|
||||
automaticUpdateChecks: true,
|
||||
sendSystemProfile: false))
|
||||
|
|
@ -92,13 +92,13 @@ fileprivate struct CheckingView: View {
|
|||
HStack(spacing: 10) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("Checking for updates…")
|
||||
Text(String(localized: "update.popover.checking", defaultValue: "Checking for updates…"))
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Cancel") {
|
||||
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
|
||||
checking.cancel()
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -120,12 +120,12 @@ fileprivate struct UpdateAvailableView: View {
|
|||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Update Available")
|
||||
Text(String(localized: "update.popover.updateAvailable", defaultValue: "Update Available"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Version:")
|
||||
Text(String(localized: "update.popover.version", defaultValue: "Version:"))
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: labelWidth, alignment: .trailing)
|
||||
Text(update.appcastItem.displayVersionString)
|
||||
|
|
@ -134,7 +134,7 @@ fileprivate struct UpdateAvailableView: View {
|
|||
|
||||
if update.appcastItem.contentLength > 0 {
|
||||
HStack(spacing: 6) {
|
||||
Text("Size:")
|
||||
Text(String(localized: "update.popover.size", defaultValue: "Size:"))
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: labelWidth, alignment: .trailing)
|
||||
Text(ByteCountFormatter.string(fromByteCount: Int64(update.appcastItem.contentLength), countStyle: .file))
|
||||
|
|
@ -144,7 +144,7 @@ fileprivate struct UpdateAvailableView: View {
|
|||
|
||||
if let date = update.appcastItem.date {
|
||||
HStack(spacing: 6) {
|
||||
Text("Released:")
|
||||
Text(String(localized: "update.popover.released", defaultValue: "Released:"))
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: labelWidth, alignment: .trailing)
|
||||
Text(date.formatted(date: .abbreviated, time: .omitted))
|
||||
|
|
@ -156,13 +156,13 @@ fileprivate struct UpdateAvailableView: View {
|
|||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Skip") {
|
||||
Button(String(localized: "common.skip", defaultValue: "Skip")) {
|
||||
update.reply(.skip)
|
||||
dismiss()
|
||||
}
|
||||
.controlSize(.small)
|
||||
|
||||
Button("Later") {
|
||||
Button(String(localized: "common.later", defaultValue: "Later")) {
|
||||
update.reply(.dismiss)
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -171,7 +171,7 @@ fileprivate struct UpdateAvailableView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button("Install and Relaunch") {
|
||||
Button(String(localized: "common.installAndRelaunch", defaultValue: "Install and Relaunch")) {
|
||||
update.reply(.install)
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -214,7 +214,7 @@ fileprivate struct DownloadingView: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Downloading Update")
|
||||
Text(String(localized: "update.popover.downloadingUpdate", defaultValue: "Downloading Update"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
if let expectedLength = download.expectedLength, expectedLength > 0 {
|
||||
|
|
@ -233,7 +233,7 @@ fileprivate struct DownloadingView: View {
|
|||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Cancel") {
|
||||
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
|
||||
download.cancel()
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -250,7 +250,7 @@ fileprivate struct ExtractingView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Preparing Update")
|
||||
Text(String(localized: "update.popover.preparingUpdate", defaultValue: "Preparing Update"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
|
|
@ -271,17 +271,17 @@ fileprivate struct InstallingView: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Restart Required")
|
||||
Text(String(localized: "update.popover.restartRequired", defaultValue: "Restart Required"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("The update is ready. Please restart the application to complete the installation.")
|
||||
Text(String(localized: "update.popover.restartRequired.message", defaultValue: "The update is ready. Please restart the application to complete the installation."))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button("Restart Later") {
|
||||
Button(String(localized: "common.restartLater", defaultValue: "Restart Later")) {
|
||||
installing.dismiss()
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -290,7 +290,7 @@ fileprivate struct InstallingView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button("Restart Now") {
|
||||
Button(String(localized: "common.restartNow", defaultValue: "Restart Now")) {
|
||||
installing.retryTerminatingApplication()
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -310,10 +310,10 @@ fileprivate struct NotFoundView: View {
|
|||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("No Updates Found")
|
||||
Text(String(localized: "update.popover.noUpdatesFound", defaultValue: "No Updates Found"))
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("You're already running the latest version.")
|
||||
Text(String(localized: "update.popover.noUpdatesFound.message", defaultValue: "You're already running the latest version."))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
@ -321,7 +321,7 @@ fileprivate struct NotFoundView: View {
|
|||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("OK") {
|
||||
Button(String(localized: "common.ok", defaultValue: "OK")) {
|
||||
notFound.acknowledgement()
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -363,7 +363,7 @@ fileprivate struct UpdateErrorView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Details")
|
||||
Text(String(localized: "update.popover.details", defaultValue: "Details"))
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
Text(details)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
|
|
@ -373,14 +373,14 @@ fileprivate struct UpdateErrorView: View {
|
|||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Copy Details") {
|
||||
Button(String(localized: "common.copyDetails", defaultValue: "Copy Details")) {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(details, forType: .string)
|
||||
}
|
||||
.controlSize(.small)
|
||||
|
||||
Button("OK") {
|
||||
Button(String(localized: "common.ok", defaultValue: "OK")) {
|
||||
error.dismiss()
|
||||
dismiss()
|
||||
}
|
||||
|
|
@ -389,7 +389,7 @@ fileprivate struct UpdateErrorView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
Button("Retry") {
|
||||
Button(String(localized: "common.retry", defaultValue: "Retry")) {
|
||||
error.retry()
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,8 +320,8 @@ struct TitlebarControlsView: View {
|
|||
iconLabel(systemName: "sidebar.left", config: config)
|
||||
}
|
||||
.accessibilityIdentifier("titlebarControl.toggleSidebar")
|
||||
.accessibilityLabel("Toggle Sidebar")
|
||||
.help(KeyboardShortcutSettings.Action.toggleSidebar.tooltip("Show or hide the sidebar"))
|
||||
.accessibilityLabel(String(localized: "titlebar.sidebar.accessibilityLabel", defaultValue: "Toggle Sidebar"))
|
||||
.help(KeyboardShortcutSettings.Action.toggleSidebar.tooltip(String(localized: "titlebar.sidebar.tooltip", defaultValue: "Show or hide the sidebar")))
|
||||
|
||||
TitlebarControlButton(config: config, action: {
|
||||
#if DEBUG
|
||||
|
|
@ -347,8 +347,8 @@ struct TitlebarControlsView: View {
|
|||
}
|
||||
.accessibilityIdentifier("titlebarControl.showNotifications")
|
||||
.background(NotificationsAnchorView { viewModel.notificationsAnchorView = $0 })
|
||||
.accessibilityLabel("Notifications")
|
||||
.help(KeyboardShortcutSettings.Action.showNotifications.tooltip("Show notifications"))
|
||||
.accessibilityLabel(String(localized: "titlebar.notifications.accessibilityLabel", defaultValue: "Notifications"))
|
||||
.help(KeyboardShortcutSettings.Action.showNotifications.tooltip(String(localized: "titlebar.notifications.tooltip", defaultValue: "Show notifications")))
|
||||
|
||||
TitlebarControlButton(config: config, action: {
|
||||
#if DEBUG
|
||||
|
|
@ -359,8 +359,8 @@ struct TitlebarControlsView: View {
|
|||
iconLabel(systemName: "plus", config: config)
|
||||
}
|
||||
.accessibilityIdentifier("titlebarControl.newTab")
|
||||
.accessibilityLabel("New Workspace")
|
||||
.help(KeyboardShortcutSettings.Action.newTab.tooltip("New workspace"))
|
||||
.accessibilityLabel(String(localized: "titlebar.newWorkspace.accessibilityLabel", defaultValue: "New Workspace"))
|
||||
.help(KeyboardShortcutSettings.Action.newTab.tooltip(String(localized: "titlebar.newWorkspace.tooltip", defaultValue: "New workspace")))
|
||||
}
|
||||
|
||||
let paddedContent = content.padding(config.groupPadding)
|
||||
|
|
@ -901,11 +901,11 @@ private struct NotificationsPopoverView: View {
|
|||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Notifications")
|
||||
Text(String(localized: "notifications.title", defaultValue: "Notifications"))
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if !notificationStore.notifications.isEmpty {
|
||||
Button("Clear All") {
|
||||
Button(String(localized: "notifications.clearAll", defaultValue: "Clear All")) {
|
||||
notificationStore.clearAll()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -921,9 +921,9 @@ private struct NotificationsPopoverView: View {
|
|||
Image(systemName: "bell.slash")
|
||||
.font(.system(size: 28))
|
||||
.foregroundColor(.secondary)
|
||||
Text("No notifications yet")
|
||||
Text(String(localized: "notifications.empty.title", defaultValue: "No notifications yet"))
|
||||
.font(.headline)
|
||||
Text("Desktop notifications will appear here.")
|
||||
Text(String(localized: "notifications.empty.subtitle", defaultValue: "Desktop notifications will appear here."))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,27 +22,29 @@ class UpdateViewModel: ObservableObject {
|
|||
case .idle:
|
||||
return ""
|
||||
case .permissionRequest:
|
||||
return "Enable Automatic Updates?"
|
||||
return String(localized: "update.permissionRequest.text", defaultValue: "Enable Automatic Updates?")
|
||||
case .checking:
|
||||
return "Checking for Updates…"
|
||||
return String(localized: "update.checking", defaultValue: "Checking for Updates…")
|
||||
case .updateAvailable(let update):
|
||||
let version = update.appcastItem.displayVersionString
|
||||
if !version.isEmpty {
|
||||
return "Update Available: \(version)"
|
||||
return String(localized: "update.available.withVersion", defaultValue: "Update Available: \(version)")
|
||||
}
|
||||
return "Update Available"
|
||||
return String(localized: "update.available.short", defaultValue: "Update Available")
|
||||
case .downloading(let download):
|
||||
if let expectedLength = download.expectedLength, expectedLength > 0 {
|
||||
let progress = Double(download.progress) / Double(expectedLength)
|
||||
return String(format: "Downloading: %.0f%%", progress * 100)
|
||||
let percent = String(format: "%.0f%%", progress * 100)
|
||||
return String(localized: "update.downloading.progress", defaultValue: "Downloading: \(percent)")
|
||||
}
|
||||
return "Downloading…"
|
||||
return String(localized: "update.downloading.status", defaultValue: "Downloading…")
|
||||
case .extracting(let extracting):
|
||||
return String(format: "Preparing: %.0f%%", extracting.progress * 100)
|
||||
let percent = String(format: "%.0f%%", extracting.progress * 100)
|
||||
return String(localized: "update.extracting.progress", defaultValue: "Preparing: \(percent)")
|
||||
case .installing(let install):
|
||||
return install.isAutoUpdate ? "Restart to Complete Update" : "Installing…"
|
||||
return install.isAutoUpdate ? String(localized: "update.restartToComplete", defaultValue: "Restart to Complete Update") : String(localized: "update.installing.status", defaultValue: "Installing…")
|
||||
case .notFound:
|
||||
return "No Updates Available"
|
||||
return String(localized: "update.noUpdates.title", defaultValue: "No Updates Available")
|
||||
case .error(let err):
|
||||
return Self.userFacingErrorTitle(for: err.error)
|
||||
}
|
||||
|
|
@ -87,19 +89,19 @@ class UpdateViewModel: ObservableObject {
|
|||
case .idle:
|
||||
return ""
|
||||
case .permissionRequest:
|
||||
return "Configure automatic update preferences"
|
||||
return String(localized: "update.configureAutoUpdates", defaultValue: "Configure automatic update preferences")
|
||||
case .checking:
|
||||
return "Please wait while we check for available updates"
|
||||
return String(localized: "update.pleaseWait", defaultValue: "Please wait while we check for available updates")
|
||||
case .updateAvailable(let update):
|
||||
return update.releaseNotes?.label ?? "Download and install the latest version"
|
||||
return update.releaseNotes?.label ?? String(localized: "update.downloadAndInstall", defaultValue: "Download and install the latest version")
|
||||
case .downloading:
|
||||
return "Downloading the update package"
|
||||
return String(localized: "update.downloadingPackage", defaultValue: "Downloading the update package")
|
||||
case .extracting:
|
||||
return "Extracting and preparing the update"
|
||||
return String(localized: "update.preparingUpdate", defaultValue: "Extracting and preparing the update")
|
||||
case let .installing(install):
|
||||
return install.isAutoUpdate ? "Restart to Complete Update" : "Installing update and preparing to restart"
|
||||
return install.isAutoUpdate ? String(localized: "update.restartToComplete", defaultValue: "Restart to Complete Update") : String(localized: "update.installingAndRestarting", defaultValue: "Installing update and preparing to restart")
|
||||
case .notFound:
|
||||
return "You are running the latest version"
|
||||
return String(localized: "update.noUpdates.message", defaultValue: "You are running the latest version")
|
||||
case .error(let err):
|
||||
return Self.userFacingErrorMessage(for: err.error)
|
||||
}
|
||||
|
|
@ -177,21 +179,21 @@ class UpdateViewModel: ObservableObject {
|
|||
if let networkError = networkError(from: nsError) {
|
||||
switch networkError.code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return "No Internet Connection"
|
||||
return String(localized: "update.error.noInternet.title", defaultValue: "No Internet Connection")
|
||||
case NSURLErrorTimedOut:
|
||||
return "Update Timed Out"
|
||||
return String(localized: "update.error.timedOut.title", defaultValue: "Update Timed Out")
|
||||
case NSURLErrorCannotFindHost:
|
||||
return "Server Not Found"
|
||||
return String(localized: "update.error.serverNotFound.title", defaultValue: "Server Not Found")
|
||||
case NSURLErrorCannotConnectToHost:
|
||||
return "Server Unreachable"
|
||||
return String(localized: "update.error.serverUnreachable.title", defaultValue: "Server Unreachable")
|
||||
case NSURLErrorNetworkConnectionLost:
|
||||
return "Connection Lost"
|
||||
return String(localized: "update.error.connectionLost.title", defaultValue: "Connection Lost")
|
||||
case NSURLErrorSecureConnectionFailed,
|
||||
NSURLErrorServerCertificateUntrusted,
|
||||
NSURLErrorServerCertificateHasBadDate,
|
||||
NSURLErrorServerCertificateHasUnknownRoot,
|
||||
NSURLErrorServerCertificateNotYetValid:
|
||||
return "Secure Connection Failed"
|
||||
return String(localized: "update.error.secureConnectionFailed.title", defaultValue: "Secure Connection Failed")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -199,24 +201,24 @@ class UpdateViewModel: ObservableObject {
|
|||
if nsError.domain == SUSparkleErrorDomain {
|
||||
switch nsError.code {
|
||||
case 4005:
|
||||
return "Updater Permission Error"
|
||||
return String(localized: "update.error.permissionError.title", defaultValue: "Updater Permission Error")
|
||||
case 2001:
|
||||
return "Couldn't Download Update"
|
||||
return String(localized: "update.error.downloadFailed.title", defaultValue: "Couldn't Download Update")
|
||||
case 1000, 1002:
|
||||
return "Update Feed Error"
|
||||
return String(localized: "update.error.feedError.title", defaultValue: "Update Feed Error")
|
||||
case 4:
|
||||
return "Invalid Update Feed"
|
||||
return String(localized: "update.error.invalidFeed.title", defaultValue: "Invalid Update Feed")
|
||||
case 3:
|
||||
return "Insecure Update Feed"
|
||||
return String(localized: "update.error.insecureFeed.title", defaultValue: "Insecure Update Feed")
|
||||
case 1, 2, 3001, 3002:
|
||||
return "Update Signature Error"
|
||||
return String(localized: "update.error.signatureError.title", defaultValue: "Update Signature Error")
|
||||
case 1003, 1005:
|
||||
return "App Location Issue"
|
||||
return String(localized: "update.error.appLocation.title", defaultValue: "App Location Issue")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return "Update Failed"
|
||||
return String(localized: "update.error.failed.title", defaultValue: "Update Failed")
|
||||
}
|
||||
|
||||
static func userFacingErrorMessage(for error: Swift.Error) -> String {
|
||||
|
|
@ -224,21 +226,21 @@ class UpdateViewModel: ObservableObject {
|
|||
if let networkError = networkError(from: nsError) {
|
||||
switch networkError.code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return "cmux can’t reach the update server. Check your internet connection and try again."
|
||||
return String(localized: "update.error.noInternet.message", defaultValue: "cmux can’t reach the update server. Check your internet connection and try again.")
|
||||
case NSURLErrorTimedOut:
|
||||
return "The update server took too long to respond. Try again in a moment."
|
||||
return String(localized: "update.error.timedOut.message", defaultValue: "The update server took too long to respond. Try again in a moment.")
|
||||
case NSURLErrorCannotFindHost:
|
||||
return "The update server can’t be found. Check your connection or try again later."
|
||||
return String(localized: "update.error.serverNotFound.message", defaultValue: "The update server can’t be found. Check your connection or try again later.")
|
||||
case NSURLErrorCannotConnectToHost:
|
||||
return "cmux couldn’t connect to the update server. Check your connection or try again later."
|
||||
return String(localized: "update.error.serverUnreachable.message", defaultValue: "cmux couldn’t connect to the update server. Check your connection or try again later.")
|
||||
case NSURLErrorNetworkConnectionLost:
|
||||
return "The network connection was lost while checking for updates. Try again."
|
||||
return String(localized: "update.error.connectionLost.message", defaultValue: "The network connection was lost while checking for updates. Try again.")
|
||||
case NSURLErrorSecureConnectionFailed,
|
||||
NSURLErrorServerCertificateUntrusted,
|
||||
NSURLErrorServerCertificateHasBadDate,
|
||||
NSURLErrorServerCertificateHasUnknownRoot,
|
||||
NSURLErrorServerCertificateNotYetValid:
|
||||
return "A secure connection to the update server couldn’t be established. Try again later."
|
||||
return String(localized: "update.error.secureConnectionFailed.message", defaultValue: "A secure connection to the update server couldn’t be established. Try again later.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -246,17 +248,17 @@ class UpdateViewModel: ObservableObject {
|
|||
if nsError.domain == SUSparkleErrorDomain {
|
||||
switch nsError.code {
|
||||
case 2001:
|
||||
return "cmux couldn't download the update feed. Check your connection and try again."
|
||||
return String(localized: "update.error.feedDownload.message", defaultValue: "cmux couldn't download the update feed. Check your connection and try again.")
|
||||
case 1000, 1002:
|
||||
return "The update feed could not be read. Please try again later."
|
||||
return String(localized: "update.error.feedRead.message", defaultValue: "The update feed could not be read. Please try again later.")
|
||||
case 4:
|
||||
return "The update feed URL is invalid. Please contact support."
|
||||
return String(localized: "update.error.invalidFeed.message", defaultValue: "The update feed URL is invalid. Please contact support.")
|
||||
case 3:
|
||||
return "The update feed is insecure. Please contact support."
|
||||
return String(localized: "update.error.insecureFeed.message", defaultValue: "The update feed is insecure. Please contact support.")
|
||||
case 1, 2, 3001, 3002:
|
||||
return "The update's signature could not be verified. Please try again later."
|
||||
return String(localized: "update.error.signatureError.message", defaultValue: "The update's signature could not be verified. Please try again later.")
|
||||
case 1003, 1005, 4005:
|
||||
return "Move cmux into Applications and relaunch to enable updates."
|
||||
return String(localized: "update.error.permissionError.message", defaultValue: "Move cmux into Applications and relaunch to enable updates.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -487,8 +489,8 @@ enum UpdateState: Equatable {
|
|||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .commit: return "View GitHub Commit"
|
||||
case .tagged: return "View Release Notes"
|
||||
case .commit: return String(localized: "update.viewGitHubCommit", defaultValue: "View GitHub Commit")
|
||||
case .tagged: return String(localized: "update.viewReleaseNotes", defaultValue: "View Release Notes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -990,10 +990,10 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
|
||||
private static func currentSplitButtonTooltips() -> BonsplitConfiguration.SplitButtonTooltips {
|
||||
BonsplitConfiguration.SplitButtonTooltips(
|
||||
newTerminal: KeyboardShortcutSettings.Action.newSurface.tooltip("New Terminal"),
|
||||
newBrowser: KeyboardShortcutSettings.Action.openBrowser.tooltip("New Browser"),
|
||||
splitRight: KeyboardShortcutSettings.Action.splitRight.tooltip("Split Right"),
|
||||
splitDown: KeyboardShortcutSettings.Action.splitDown.tooltip("Split Down")
|
||||
newTerminal: KeyboardShortcutSettings.Action.newSurface.tooltip(String(localized: "workspace.tooltip.newTerminal", defaultValue: "New Terminal")),
|
||||
newBrowser: KeyboardShortcutSettings.Action.openBrowser.tooltip(String(localized: "workspace.tooltip.newBrowser", defaultValue: "New Browser")),
|
||||
splitRight: KeyboardShortcutSettings.Action.splitRight.tooltip(String(localized: "workspace.tooltip.splitRight", defaultValue: "Split Right")),
|
||||
splitDown: KeyboardShortcutSettings.Action.splitDown.tooltip(String(localized: "workspace.tooltip.splitDown", defaultValue: "Split Down"))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3331,15 +3331,15 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
let panel = panels[panelId] else { return }
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Rename Tab"
|
||||
alert.informativeText = "Enter a custom name for this tab."
|
||||
alert.messageText = String(localized: "dialog.renameTab.title", defaultValue: "Rename Tab")
|
||||
alert.informativeText = String(localized: "dialog.renameTab.message", defaultValue: "Enter a custom name for this tab.")
|
||||
let currentTitle = panelCustomTitles[panelId] ?? panelTitles[panelId] ?? panel.displayTitle
|
||||
let input = NSTextField(string: currentTitle)
|
||||
input.placeholderString = "Tab name"
|
||||
input.placeholderString = String(localized: "dialog.renameTab.placeholder", defaultValue: "Tab name")
|
||||
input.frame = NSRect(x: 0, y: 0, width: 240, height: 22)
|
||||
alert.accessoryView = input
|
||||
alert.addButton(withTitle: "Rename")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "common.rename", defaultValue: "Rename"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
let alertWindow = alert.window
|
||||
alertWindow.initialFirstResponder = input
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -3368,24 +3368,24 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
)
|
||||
|
||||
var options: [(title: String, destination: PanelMoveDestination)] = [
|
||||
("New Workspace in Current Window", .newWorkspaceInCurrentWindow),
|
||||
("Selected Workspace in New Window", .selectedWorkspaceInNewWindow),
|
||||
(String(localized: "dialog.moveTab.newWorkspaceCurrentWindow", defaultValue: "New Workspace in Current Window"), .newWorkspaceInCurrentWindow),
|
||||
(String(localized: "dialog.moveTab.selectedWorkspaceNewWindow", defaultValue: "Selected Workspace in New Window"), .selectedWorkspaceInNewWindow),
|
||||
]
|
||||
options.append(contentsOf: workspaceTargets.map { target in
|
||||
(target.label, .existingWorkspace(target.workspaceId))
|
||||
})
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Move Tab"
|
||||
alert.informativeText = "Choose a destination for this tab."
|
||||
alert.messageText = String(localized: "dialog.moveTab.title", defaultValue: "Move Tab")
|
||||
alert.informativeText = String(localized: "dialog.moveTab.message", defaultValue: "Choose a destination for this tab.")
|
||||
let popup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 320, height: 26), pullsDown: false)
|
||||
for option in options {
|
||||
popup.addItem(withTitle: option.title)
|
||||
}
|
||||
popup.selectItem(at: 0)
|
||||
alert.accessoryView = popup
|
||||
alert.addButton(withTitle: "Move")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "dialog.moveTab.move", defaultValue: "Move"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
|
||||
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
||||
let selectedIndex = max(0, min(popup.indexOfSelectedItem, options.count - 1))
|
||||
|
|
@ -3431,9 +3431,9 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
if !moved {
|
||||
let failure = NSAlert()
|
||||
failure.alertStyle = .warning
|
||||
failure.messageText = "Move Failed"
|
||||
failure.informativeText = "cmux could not move this tab to the selected destination."
|
||||
failure.addButton(withTitle: "OK")
|
||||
failure.messageText = String(localized: "dialog.moveFailed.title", defaultValue: "Move Failed")
|
||||
failure.informativeText = String(localized: "dialog.moveFailed.message", defaultValue: "cmux could not move this tab to the selected destination.")
|
||||
failure.addButton(withTitle: String(localized: "common.ok", defaultValue: "OK"))
|
||||
_ = failure.runModal()
|
||||
}
|
||||
}
|
||||
|
|
@ -3500,11 +3500,11 @@ extension Workspace: BonsplitDelegate {
|
|||
@MainActor
|
||||
private func confirmClosePanel(for tabId: TabID) async -> Bool {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Close tab?"
|
||||
alert.informativeText = "This will close the current tab."
|
||||
alert.messageText = String(localized: "dialog.closeTab.title", defaultValue: "Close tab?")
|
||||
alert.informativeText = String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab.")
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "Close")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "dialog.closeTab.close", defaultValue: "Close"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
|
||||
// Prefer a sheet if we can find a window, otherwise fall back to modal.
|
||||
if let window = NSApp.keyWindow ?? NSApp.mainWindow {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue