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

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