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:
atani 2026-03-05 07:58:28 +09:00 committed by GitHub
parent 422c86e822
commit 2c330efb8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 11643 additions and 789 deletions

View file

@ -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 = (

View 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) ウインドウを作成"
}
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")
let escapeHTML: (String) -> String = { value in
value
.replacingOccurrences(of: "&", with: "&amp;")
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
.replacingOccurrences(of: "\"", with: "&quot;")
}
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 ?? ""

View file

@ -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"))
}
}

View file

@ -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: ""
)

View file

@ -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()

View file

@ -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 }
}

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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 cant reach the update server. Check your internet connection and try again."
return String(localized: "update.error.noInternet.message", defaultValue: "cmux cant 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 cant be found. Check your connection or try again later."
return String(localized: "update.error.serverNotFound.message", defaultValue: "The update server cant be found. Check your connection or try again later.")
case NSURLErrorCannotConnectToHost:
return "cmux couldnt connect to the update server. Check your connection or try again later."
return String(localized: "update.error.serverUnreachable.message", defaultValue: "cmux couldnt 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 couldnt be established. Try again later."
return String(localized: "update.error.secureConnectionFailed.message", defaultValue: "A secure connection to the update server couldnt 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")
}
}
}

View file

@ -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