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
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue