diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackFrames.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackFrames.swift new file mode 100644 index 00000000000..d30f5cbac3d --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackFrames.swift @@ -0,0 +1,29 @@ +/// Media playback across a page's frames, folded from the per-frame reports +/// of the injected media-playback hook. +public struct BrowserMediaPlaybackFrames: Equatable, Sendable { + private var playingFrameIDs: Set = [] + private var audibleFrameIDs: Set = [] + private var pictureInPictureFrameIDs: Set = [] + + public init() {} + + /// Whether any frame has actively playing media. + public var isPlaying: Bool { !playingFrameIDs.isEmpty } + /// Whether any frame is playing media that is audible. + public var isAudible: Bool { !audibleFrameIDs.isEmpty } + /// Whether any frame has a video in Picture in Picture, playing or paused. + public var isPictureInPictureActive: Bool { !pictureInPictureFrameIDs.isEmpty } + + /// Replaces the reporting frame's state with the report. + public mutating func apply(_ report: BrowserMediaPlaybackReport) { + playingFrameIDs.update(report.frameID, isMember: report.isPlaying) + audibleFrameIDs.update(report.frameID, isMember: report.isPlaying && report.isAudible) + pictureInPictureFrameIDs.update(report.frameID, isMember: report.isPictureInPicture) + } +} + +private extension Set { + mutating func update(_ frameID: String, isMember: Bool) { + if isMember { insert(frameID) } else { remove(frameID) } + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackMessageHandler.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackMessageHandler.swift index ed160093fbf..61342b94cee 100644 --- a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackMessageHandler.swift +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackMessageHandler.swift @@ -1,15 +1,24 @@ public import Foundation public import WebKit -/// Receives `{ frameID, playing, audible }` from the injected media-playback hook and +/// Receives `{ frameID, playing, audible, pip }` from the injected media-playback hook and /// forwards it to the owning ``BrowserPanel`` on the main actor. /// /// Mirrors ``ReactGrabMessageHandler``: a thin `NSObject` adapter so the panel -/// itself never has to conform to `WKScriptMessageHandler`. +/// itself never has to conform to `WKScriptMessageHandler`. It is bound to one +/// web view: a popup the page opens is built from the opener's configuration +/// and shares its content controller, so the popup's reports reach this handler +/// too and are dropped. +@MainActor public final class BrowserMediaPlaybackMessageHandler: NSObject, WKScriptMessageHandler { + private weak var webView: WKWebView? private let onReport: @MainActor (BrowserMediaPlaybackReport) -> Void - public init(onReport: @escaping @MainActor (BrowserMediaPlaybackReport) -> Void) { + public init( + webView: WKWebView, + onReport: @escaping @MainActor (BrowserMediaPlaybackReport) -> Void + ) { + self.webView = webView self.onReport = onReport } @@ -17,19 +26,22 @@ public final class BrowserMediaPlaybackMessageHandler: NSObject, WKScriptMessage _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { - guard let body = message.body as? [String: Any], + guard message.webView === webView, + let body = message.body as? [String: Any], let frameID = body["frameID"] as? String, let playing = body["playing"] as? Bool else { return } - let audible = body["audible"] as? Bool ?? false - let report = BrowserMediaPlaybackReport(frameID: frameID, isPlaying: playing, isAudible: audible) + let report = BrowserMediaPlaybackReport( + frameID: frameID, + isPlaying: playing, + isAudible: body["audible"] as? Bool ?? false, + isPictureInPicture: body["pip"] as? Bool ?? false + ) // WebKit delivers script messages on the main thread. Apply the report // synchronously instead of hopping through a `Task` so it lands in // WebKit's delivery order relative to navigation callbacks: a report // emitted by a document before it navigates away is applied before the // matching `didCommit` reset, so a stale `playing: true` cannot re-add a // dead frame id after the reset and pin the pane against discard. - MainActor.assumeIsolated { - onReport(report) - } + onReport(report) } } diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackReport.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackReport.swift index 7b02071ad53..323bad5286a 100644 --- a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackReport.swift +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/MediaPlayback/BrowserMediaPlaybackReport.swift @@ -9,14 +9,18 @@ public struct BrowserMediaPlaybackReport: Sendable { public let isPlaying: Bool /// Whether that frame currently has an unmuted, non-zero-volume audio source. public let isAudible: Bool + /// Whether that frame has a video in Picture in Picture, playing or paused. + public let isPictureInPicture: Bool public init( frameID: String, isPlaying: Bool, - isAudible: Bool + isAudible: Bool, + isPictureInPicture: Bool = false ) { self.frameID = frameID self.isPlaying = isPlaying self.isAudible = isAudible + self.isPictureInPicture = isPictureInPicture } } diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserDiscardRestoreStrategy.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserDiscardRestoreStrategy.swift new file mode 100644 index 00000000000..b356dca6aee --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserDiscardRestoreStrategy.swift @@ -0,0 +1,89 @@ +public import Foundation + +/// How a pane that was discarded for memory brings its page back when it is +/// shown again. +/// +/// WebKit's `interactionState` carries the native back/forward list together +/// with each entry's scroll and view state, so assigning it to the replacement +/// web view returns the page the user left, like a Chrome tab discard. Loading +/// the URL from scratch is the fallback for panes whose captured state cannot +/// be replayed safely. +public enum BrowserDiscardRestoreStrategy: Equatable, Sendable { + /// Assign the captured WebKit session state to the replacement web view. + case restoreInteractionState(Data) + /// Load the restore URL from scratch. + case replayURL(URL) + + /// Pane conditions that force the URL fallback. + public struct Conditions: Equatable, Sendable { + /// The user asked for a reload, which must fetch a fresh document. + public var isExplicitReload: Bool + /// A remote workspace pane. Its pages load through a loopback proxy + /// whose endpoint can change across reconnects, so session state would + /// replay stale proxy URLs. The URL path also queues until the + /// endpoint is up. + public var usesRemoteWorkspaceProxy: Bool + /// Cloud browser routing owns its own connection flow. + public var usesCloudAccessRouting: Bool + /// The web content process died and recovery replaces the web view. + public var hasRecoverableWebContentTermination: Bool + /// An http page the insecure-HTTP gate would stop. Session state + /// replays as a back/forward load, which asks again; the URL path + /// reopens a page the user already chose to open without asking. + public var requiresInsecureHTTPConsent: Bool + + public init( + isExplicitReload: Bool = false, + usesRemoteWorkspaceProxy: Bool = false, + usesCloudAccessRouting: Bool = false, + hasRecoverableWebContentTermination: Bool = false, + requiresInsecureHTTPConsent: Bool = false + ) { + self.isExplicitReload = isExplicitReload + self.usesRemoteWorkspaceProxy = usesRemoteWorkspaceProxy + self.usesCloudAccessRouting = usesCloudAccessRouting + self.hasRecoverableWebContentTermination = hasRecoverableWebContentTermination + self.requiresInsecureHTTPConsent = requiresInsecureHTTPConsent + } + + var forcesURLReplay: Bool { + isExplicitReload + || usesRemoteWorkspaceProxy + || usesCloudAccessRouting + || hasRecoverableWebContentTermination + || requiresInsecureHTTPConsent + } + } + + /// Whether session state may be captured, persisted or restored for a + /// document at `url`. Web and local file documents qualify; the caller + /// re-grants a local file's trust before assigning the state. App-internal + /// documents such as the diff viewer resolve through their own handlers, + /// which a back/forward replay would bypass. + public static func canRestoreSessionState(for url: URL?) -> Bool { + guard let scheme = url?.scheme?.lowercased() else { return false } + return scheme == "http" || scheme == "https" || scheme == "file" + } + + /// Picks the restore path for `restoreURL`. Captured state is used only + /// when it was taken for the same page the pane is about to restore, so a + /// navigation issued while the pane was discarded always wins. A page + /// shown as a form submission result loads by URL, because assigning its + /// state would send the form again. + public static func resolve( + restoreURL: URL, + capture: BrowserPageStateCapture?, + conditions: Conditions = Conditions() + ) -> BrowserDiscardRestoreStrategy { + guard !conditions.forcesURLReplay, + canRestoreSessionState(for: restoreURL), + let capture, + capture.anchorURL == restoreURL, + !capture.documentHasFormSubmission, + let interactionState = capture.interactionState, + !interactionState.isEmpty else { + return .replayURL(restoreURL) + } + return .restoreInteractionState(interactionState) + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserFormStateMessageHandler.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserFormStateMessageHandler.swift new file mode 100644 index 00000000000..284b2fd1dd7 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserFormStateMessageHandler.swift @@ -0,0 +1,37 @@ +public import Foundation +public import WebKit + +/// Receives unsaved form input from the injected +/// ``WKUserScript/browserFormStateObserver()`` and forwards it to the +/// owning panel on the main actor. +/// +/// A thin `NSObject` adapter so the panel never conforms to +/// `WKScriptMessageHandler` itself. It is bound to one web view: a popup the +/// page opens is built from the opener's configuration and shares its content +/// controller, so the popup's reports reach this handler too and are dropped. +@MainActor +public final class BrowserFormStateMessageHandler: NSObject, WKScriptMessageHandler { + private weak var webView: WKWebView? + private let onReport: @MainActor (BrowserFormStateSnapshot) -> Void + + public init( + webView: WKWebView, + onReport: @escaping @MainActor (BrowserFormStateSnapshot) -> Void + ) { + self.webView = webView + self.onReport = onReport + } + + public func userContentController( + _ userContentController: WKUserContentController, + didReceive message: WKScriptMessage + ) { + guard message.frameInfo.isMainFrame, + message.webView === webView, + let snapshot = BrowserFormStateSnapshot(messageBody: message.body) else { return } + // WebKit delivers script messages on the main thread, in order with + // navigation callbacks, so a report sent by a document before it + // navigates away lands before the next document's commit resets it. + onReport(snapshot) + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserFormStateSnapshot.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserFormStateSnapshot.swift new file mode 100644 index 00000000000..5bb5a6bf456 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserFormStateSnapshot.swift @@ -0,0 +1,147 @@ +public import Foundation + +/// Unsaved input in the main frame's form controls, reported by the injected +/// form-state observer. +/// +/// WebKit's `interactionState` only carries form values for history entries +/// the user navigated away from, so the current page's typed input would be +/// lost when a discarded pane restores. This snapshot fills that gap. It is +/// kept in memory only and never written to the session file. Password, +/// payment, one-time-code and `autocomplete="off"` fields are never reported; +/// a change to one only sets ``hasUnrestorableInput``. +public struct BrowserFormStateSnapshot: Equatable, Sendable { + /// Largest number of fields kept per document. + public static let maxFieldCount = 200 + /// Largest value, in UTF-16 code units, kept per field. + public static let maxValueLength = 64 * 1024 + + public struct Field: Equatable, Sendable { + /// Stable locator for the control: `id:`, `name:` or `path:` prefixed. + public var key: String + /// Text value for text-like inputs and text areas. + public var value: String? + /// Checked state for checkboxes and radio buttons. + public var isChecked: Bool? + /// Selected option indexes for select elements. + public var selectedOptionIndexes: [Int]? + + public init(key: String, value: String? = nil, isChecked: Bool? = nil, selectedOptionIndexes: [Int]? = nil) { + self.key = key + self.value = value + self.isChecked = isChecked + self.selectedOptionIndexes = selectedOptionIndexes + } + } + + /// URL of the document the fields belong to. + public var documentURL: URL + public var fields: [Field] + /// Whether the document also holds typed input a restore cannot replay, + /// such as a password, a file selection or a rich-text edit. + public var hasUnrestorableInput: Bool + + public init(documentURL: URL, fields: [Field], hasUnrestorableInput: Bool = false) { + self.documentURL = documentURL + self.fields = fields + self.hasUnrestorableInput = hasUnrestorableInput + } + + /// Parses `{ url, fields: [{ k, v?, c?, s? }], unrestorable? }` from the + /// observer. Returns nil for a malformed body. Oversized values and fields + /// past ``maxFieldCount`` are dropped and count as unrestorable input. + public init?(messageBody: Any) { + guard let body = messageBody as? [String: Any], + let urlString = body["url"] as? String, + let documentURL = URL(string: urlString), + let rawFields = body["fields"] as? [Any] else { + return nil + } + var fields: [Field] = [] + var hasUnrestorableInput = body["unrestorable"] as? Bool ?? false + for rawField in rawFields { + guard fields.count < Self.maxFieldCount else { + hasUnrestorableInput = true + break + } + guard let entry = rawField as? [String: Any], + let key = entry["k"] as? String, + !key.isEmpty else { continue } + let field: Field + if let value = entry["v"] as? String { + guard value.utf16.count <= Self.maxValueLength else { + hasUnrestorableInput = true + continue + } + field = Field(key: key, value: value) + } else if let checked = entry["c"] as? Bool { + field = Field(key: key, isChecked: checked) + } else if let selected = entry["s"] as? [Any] { + let indexes = selected.compactMap { ($0 as? NSNumber)?.intValue } + guard indexes.count == selected.count else { + hasUnrestorableInput = true + continue + } + field = Field(key: key, selectedOptionIndexes: indexes) + } else { + continue + } + fields.append(field) + } + self.init(documentURL: documentURL, fields: fields, hasUnrestorableInput: hasUnrestorableInput) + } + + /// Whether there are no fields to restore. + public var isEmpty: Bool { fields.isEmpty } + + /// Whether the fields were typed on the same origin as `url`. Values are + /// never carried to another site. The report URL can trail the document + /// URL after a same-document route change, so paths are not compared, + /// except for file URLs, whose origin is the file itself. + public func sharesOrigin(with url: URL?) -> Bool { + guard let url else { return false } + if documentURL.isFileURL || url.isFileURL { + return documentURL.isFileURL && url.isFileURL && Self.isSameDocument(documentURL, url) + } + guard let scheme = documentURL.scheme?.lowercased(), let host = documentURL.host?.lowercased() else { + return false + } + guard scheme == url.scheme?.lowercased(), host == url.host?.lowercased() else { return false } + let defaultPort = scheme == "https" ? 443 : scheme == "http" ? 80 : nil + return (documentURL.port ?? defaultPort) == (url.port ?? defaultPort) + } + + /// Whether two URLs load the same document. Fragment changes keep the + /// same document, so they are ignored. + public static func isSameDocument(_ lhs: URL, _ rhs: URL) -> Bool { + documentIdentity(lhs) == documentIdentity(rhs) + } + + /// Fields in the shape the restore script expects as its `fields` argument. + public var restorePayload: [[String: Any]] { + fields.map { field in + var entry: [String: Any] = ["k": field.key] + if let value = field.value { + entry["v"] = value + } else if let isChecked = field.isChecked { + entry["c"] = isChecked + } else if let selectedOptionIndexes = field.selectedOptionIndexes { + entry["s"] = selectedOptionIndexes + } + return entry + } + } + + static func documentIdentity(_ url: URL) -> String { + if url.isFileURL { + let standardized = url.standardizedFileURL + let host = url.host?.lowercased() ?? "" + let port = url.port.map(String.init) ?? "" + return "\(host):\(port):\(standardized.path)" + } + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return url.absoluteString + } + components.fragment = nil + return components.string ?? url.absoluteString + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageRestorationState.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageRestorationState.swift new file mode 100644 index 00000000000..e4eaefd4fb2 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageRestorationState.swift @@ -0,0 +1,264 @@ +public import Foundation + +/// A browser pane's page state across a web view discard. +/// +/// While the page is live it collects what WebKit's session state leaves +/// out: unsaved form input, whether any history entry was a form submission, +/// and a snapshot taken when the pane is hidden. A discard folds these into +/// one ``BrowserPageStateCapture``. The capture lives until the replacement +/// web view commits a document. If that commit is the restore, the capture's +/// form input waits for the load to finish; any other commit drops it. +@MainActor +public final class BrowserPageRestorationState { + /// How a discarded page was brought back. + public enum RestoreMethod: Equatable, Sendable { + /// WebKit session state was assigned to the replacement web view. + case interactionState + /// The restore URL was loaded again. + case urlReplay + } + + /// A commit that completed the restore of a discarded page. + public struct RestoredCommit: Equatable, Sendable { + public var method: RestoreMethod + /// Whether the restored back/forward list is the pane's whole history. + public var coversNavigationHistory: Bool + + public init(method: RestoreMethod, coversNavigationHistory: Bool) { + self.method = method + self.coversNavigationHistory = coversNavigationHistory + } + } + + /// Unsaved input the current document last reported, or nil when none. + public private(set) var liveFormState: BrowserFormStateSnapshot? + /// Whether the current web view's back/forward list holds a form + /// submission. Sticky until the list is replaced. + public private(set) var liveContainsFormSubmission = false + /// Whether the current document, or one of its frames, came from a form + /// submission. + private var liveDocumentHasFormSubmission = false + /// Whether the main-frame navigation that has not committed yet submits + /// a form. A redirect re-decides it. + private var pendingDocumentHasFormSubmission = false + /// State captured at the last discard, until a document commits. + public private(set) var discardedCapture: BrowserPageStateCapture? + /// The restore issued for ``discardedCapture`` that has not committed. + public private(set) var inFlightRestore: RestoreMethod? + /// Form input to apply once the restored document finishes loading. + public private(set) var pendingFormRestore: BrowserFormStateSnapshot? + + private var hiddenSnapshotToken: UUID? + private var hiddenSnapshot: BrowserPageSnapshotImage? + + /// Script message handler registered for the current web view. + public var formStateMessageHandler: BrowserFormStateMessageHandler? + /// Snapshot painted over the replacement web view while it restores. + public var overlayView: BrowserPageSnapshotOverlayView? + + public init() {} + + // MARK: Live page + + public func recordLiveFormState(_ snapshot: BrowserFormStateSnapshot) { + liveFormState = snapshot.isEmpty && !snapshot.hasUnrestorableInput ? nil : snapshot + } + + /// Whether the current document holds typed input a restore would lose. + public var hasUnrestorableLiveInput: Bool { + liveFormState?.hasUnrestorableInput ?? false + } + + /// Call for every navigation request allowed in this web view, including + /// each redirect. WebKit keeps a form submission's request body in the + /// entry's session state. + /// + /// - Parameters: + /// - targetFrameIsMainFrame: Whether the request loads in the main frame + /// or a subframe, or nil when it has no target frame. Such a request + /// opens a new window, so it is ignored. + /// - isFormSubmission: Whether the request sends a body, such as a POST. + public func noteNavigationRequest(targetFrameIsMainFrame: Bool?, isFormSubmission: Bool) { + guard let isMainFrame = targetFrameIsMainFrame else { return } + if isFormSubmission { + liveContainsFormSubmission = true + } + if isMainFrame { + pendingDocumentHasFormSubmission = isFormSubmission + } else if isFormSubmission { + liveDocumentHasFormSubmission = true + } + } + + // MARK: Hidden snapshot + + /// Starts a snapshot for a pane that was just hidden and returns the + /// token its completion must present. + public func beginHiddenSnapshot() -> UUID { + let token = UUID() + hiddenSnapshotToken = token + hiddenSnapshot = nil + return token + } + + /// Stores a finished snapshot. A snapshot that completes after the pane + /// was discarded still attaches to that discard's capture. + public func completeHiddenSnapshot(token: UUID, image: BrowserPageSnapshotImage?) { + guard let image else { return } + if hiddenSnapshotToken == token { + hiddenSnapshot = image + } else if var capture = discardedCapture, capture.snapshotToken == token, capture.snapshot == nil { + capture.snapshot = image + discardedCapture = capture + } + } + + /// Forgets the hidden snapshot, for a pane shown again before a discard. + public func cancelHiddenSnapshot() { + hiddenSnapshotToken = nil + hiddenSnapshot = nil + } + + // MARK: Discard and restore + + /// Folds the live state into a capture as the web view is dropped. + /// + /// - Parameters: + /// - interactionState: The dropped web view's session state, when it + /// showed a replayable page. + /// - documentURL: URL of that web view's current history entry. + /// - anchorURL: The URL the pane will restore to. + /// - coversNavigationHistory: Whether the native back/forward list was + /// the pane's history. + public func recordDiscard( + interactionState: Data?, + documentURL: URL?, + anchorURL: URL?, + coversNavigationHistory: Bool + ) { + defer { + liveFormState = nil + liveContainsFormSubmission = false + liveDocumentHasFormSubmission = false + pendingDocumentHasFormSubmission = false + cancelHiddenSnapshot() + inFlightRestore = nil + pendingFormRestore = nil + } + // A web view dropped before its restore committed holds nothing newer + // than the capture it was restoring. + guard discardedCapture == nil else { return } + let formState = liveFormState.flatMap { $0.sharesOrigin(with: documentURL) ? $0 : nil } + discardedCapture = BrowserPageStateCapture( + interactionState: interactionState, + documentURL: documentURL, + anchorURL: anchorURL, + formState: formState, + snapshot: hiddenSnapshot, + snapshotToken: hiddenSnapshotToken, + coversNavigationHistory: coversNavigationHistory, + containsFormSubmission: liveContainsFormSubmission, + documentHasFormSubmission: liveDocumentHasFormSubmission + ) + } + + /// Seeds the capture from session state saved by a previous launch. Such + /// state was only saved when it covered the pane's history and held no + /// form submission. + public func seedFromSessionState(_ interactionState: Data, documentURL: URL?, anchorURL: URL?) { + guard !interactionState.isEmpty else { return } + discardedCapture = BrowserPageStateCapture( + interactionState: interactionState, + documentURL: documentURL, + anchorURL: anchorURL, + coversNavigationHistory: true, + containsFormSubmission: false + ) + } + + /// Call when any main-frame navigation starts. The restore recorded after + /// this call is the one in flight; any later navigation supersedes it, so + /// its commit does not bring back the capture's input. + public func noteNavigationStarted() { + inFlightRestore = nil + } + + /// Records the restore issued for the discarded capture. + public func noteRestoreStarted(_ method: RestoreMethod) { + guard discardedCapture != nil else { return } + inFlightRestore = method + } + + /// Drops the capture so the next restore reloads from the network, for + /// an explicit reload. + public func dropCapture() { + discardedCapture = nil + inFlightRestore = nil + pendingFormRestore = nil + } + + /// Drops the capture for a pane brought back without a restore, such as + /// by a history navigation. + /// + /// - Returns: Whether the web view's back/forward list already holds the + /// pane's whole history because session state was assigned to it. + @discardableResult + public func noteReactivatedWithoutRestore() -> Bool { + let restoredHistory = inFlightRestore == .interactionState + && discardedCapture?.coversNavigationHistory == true + dropCapture() + return restoredHistory + } + + /// Call for every main-frame document commit except the browser's own + /// error page, which keeps the capture for the next restore attempt. + /// + /// - Parameter isDiscardRestoreCommit: Whether the commit is the discarded + /// page coming back rather than a navigation somewhere else. + /// - Returns: The restore the commit completed, or nil when it completed + /// none. + @discardableResult + public func noteDocumentCommitted(isDiscardRestoreCommit: Bool) -> RestoredCommit? { + liveDocumentHasFormSubmission = pendingDocumentHasFormSubmission + pendingDocumentHasFormSubmission = false + liveFormState = nil + pendingFormRestore = nil + let method = inFlightRestore + inFlightRestore = nil + guard let capture = discardedCapture else { return nil } + discardedCapture = nil + guard isDiscardRestoreCommit, let method else { return nil } + if method == .interactionState { + liveContainsFormSubmission = capture.containsFormSubmission + } + pendingFormRestore = capture.formState + return RestoredCommit(method: method, coversNavigationHistory: capture.coversNavigationHistory) + } + + /// Returns and clears form input waiting for the document at `url`. + /// Input is never applied to another origin. + public func takePendingFormRestore(for url: URL?) -> BrowserFormStateSnapshot? { + defer { pendingFormRestore = nil } + guard let pendingFormRestore, !pendingFormRestore.isEmpty, pendingFormRestore.sharesOrigin(with: url) else { + return nil + } + return pendingFormRestore + } + + /// Puts a failed form restore back so a later document milestone can retry it. + public func retryPendingFormRestore(_ snapshot: BrowserFormStateSnapshot) { + pendingFormRestore = snapshot + } + + /// Session state that may be written to the session file for a pane whose + /// web view is discarded, or nil when the capture may not be persisted. + public func persistableDiscardedInteractionState() -> Data? { + discardedCapture?.persistableInteractionState() + } + + /// Removes the restore overlay, if one is showing. + public func dismissOverlay() { + overlayView?.dismiss() + overlayView = nil + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageSnapshotOverlayView.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageSnapshotOverlayView.swift new file mode 100644 index 00000000000..dca640b090c --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageSnapshotOverlayView.swift @@ -0,0 +1,100 @@ +public import AppKit + +/// Paints the last snapshot of a discarded page over its replacement web view +/// until the restored document paints, with a small "Restoring" badge so the +/// user can tell the page is not live yet. +/// +/// The overlay never takes mouse events: clicks, scrolls and drags reach the +/// web view underneath, which is already loading the restored page. +@MainActor +public final class BrowserPageSnapshotOverlayView: NSView { + private let imageView = NSImageView() + private let badge = NSView() + private let spinner = NSProgressIndicator() + private let label = NSTextField(labelWithString: "") + + public override var isFlipped: Bool { true } + public override var isOpaque: Bool { false } + + /// - Parameters: + /// - snapshot: The page as it looked before the discard, or nil to show + /// only the badge. + /// - restoringLabel: Localized badge text supplied by the app. + public init(snapshot: BrowserPageSnapshotImage?, restoringLabel: String) { + super.init(frame: .zero) + wantsLayer = true + autoresizingMask = [.width, .height] + setAccessibilityElement(true) + setAccessibilityRole(.progressIndicator) + setAccessibilityLabel(restoringLabel) + + if let snapshot, let image = NSImage(data: snapshot.jpegData) { + image.size = snapshot.pointSize + imageView.image = image + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.imageAlignment = .alignCenter + imageView.imageFrameStyle = .none + imageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(imageView) + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + badge.wantsLayer = true + badge.layer?.cornerRadius = 11 + badge.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.65).cgColor + badge.translatesAutoresizingMaskIntoConstraints = false + spinner.style = .spinning + spinner.controlSize = .small + spinner.appearance = NSAppearance(named: .darkAqua) + spinner.isDisplayedWhenStopped = false + spinner.translatesAutoresizingMaskIntoConstraints = false + label.stringValue = restoringLabel + label.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .medium) + label.textColor = .white + label.translatesAutoresizingMaskIntoConstraints = false + badge.addSubview(spinner) + badge.addSubview(label) + addSubview(badge) + NSLayoutConstraint.activate([ + badge.centerXAnchor.constraint(equalTo: centerXAnchor), + badge.topAnchor.constraint(equalTo: topAnchor, constant: 12), + badge.heightAnchor.constraint(equalToConstant: 22), + spinner.leadingAnchor.constraint(equalTo: badge.leadingAnchor, constant: 8), + spinner.centerYAnchor.constraint(equalTo: badge.centerYAnchor), + spinner.widthAnchor.constraint(equalToConstant: 12), + spinner.heightAnchor.constraint(equalToConstant: 12), + label.leadingAnchor.constraint(equalTo: spinner.trailingAnchor, constant: 6), + label.trailingAnchor.constraint(equalTo: badge.trailingAnchor, constant: -10), + label.centerYAnchor.constraint(equalTo: badge.centerYAnchor) + ]) + spinner.startAnimation(nil) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + nil + } + + /// Whether the overlay carries a page snapshot, not just the badge. + public var showsSnapshot: Bool { imageView.image != nil } + + public override func hitTest(_ point: NSPoint) -> NSView? { + nil + } + + /// Covers `webView`'s bounds and follows its size. + public func install(over webView: NSView) { + frame = webView.bounds + webView.addSubview(self, positioned: .above, relativeTo: nil) + } + + public func dismiss() { + spinner.stopAnimation(nil) + removeFromSuperview() + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageStateCapture.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageStateCapture.swift new file mode 100644 index 00000000000..8af9a21b252 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/BrowserPageStateCapture.swift @@ -0,0 +1,101 @@ +public import Foundation + +/// The page state a browser pane keeps when its web view is dropped, so the +/// replacement can come back where the user left it. +public struct BrowserPageStateCapture: Equatable, Sendable { + /// Upper bound for session state written to the session file. Typical + /// pages serialize to a few kilobytes; very long histories are dropped + /// rather than bloating every autosave. + public static let defaultPersistableInteractionStateByteLimit = 256 * 1024 + + /// WebKit `interactionState`: the back/forward list plus each entry's + /// scroll and view state. Nil when no replayable page was showing. + public var interactionState: Data? + /// URL of the document WebKit had committed when the state was captured. + /// This is the URL WebKit loads on restore, which can differ from the + /// display URL (remote proxy loopback aliases). + public var documentURL: URL? + /// The restore URL the pane resolves right after the discard. Restore uses + /// the captured state only while the pane still points at this URL. + public var anchorURL: URL? + /// Unsaved form input from the main frame, kept in memory only. + public var formState: BrowserFormStateSnapshot? + /// JPEG of the page painted while the restore loads. + public var snapshot: BrowserPageSnapshotImage? + /// Token of the snapshot request whose result belongs to this capture. + public var snapshotToken: UUID? + /// Whether the native back/forward list is the pane's history. False while + /// the pane replays URL history restored from a previous launch, which the + /// restore must then keep. + public var coversNavigationHistory: Bool + /// Whether any entry came from a form submission. WebKit serializes POST + /// bodies into session state, so such state never reaches disk. + public var containsFormSubmission: Bool + /// Whether the current entry, or one of its frames, came from a form + /// submission, so assigning the state would submit the form again. + public var documentHasFormSubmission: Bool + + public init( + interactionState: Data?, + documentURL: URL?, + anchorURL: URL? = nil, + formState: BrowserFormStateSnapshot? = nil, + snapshot: BrowserPageSnapshotImage? = nil, + snapshotToken: UUID? = nil, + coversNavigationHistory: Bool, + containsFormSubmission: Bool, + documentHasFormSubmission: Bool = false + ) { + self.interactionState = interactionState + self.documentURL = documentURL + self.anchorURL = anchorURL + self.formState = formState + self.snapshot = snapshot + self.snapshotToken = snapshotToken + self.coversNavigationHistory = coversNavigationHistory + self.containsFormSubmission = containsFormSubmission + self.documentHasFormSubmission = documentHasFormSubmission + } + + /// Session state that may be written to the session file, or nil when it + /// is missing, too large, replays restored URL history, or holds a form + /// submission. + public func persistableInteractionState( + maxBytes: Int = defaultPersistableInteractionStateByteLimit + ) -> Data? { + Self.persistableInteractionState( + interactionState, + coversNavigationHistory: coversNavigationHistory, + containsFormSubmission: containsFormSubmission || formState?.isEmpty == false, + maxBytes: maxBytes + ) + } + + /// Shared persistence gate for captured and live session state. + public static func persistableInteractionState( + _ interactionState: Data?, + coversNavigationHistory: Bool, + containsFormSubmission: Bool, + maxBytes: Int = defaultPersistableInteractionStateByteLimit + ) -> Data? { + guard let interactionState, + !interactionState.isEmpty, + interactionState.count <= maxBytes, + coversNavigationHistory, + !containsFormSubmission else { + return nil + } + return interactionState + } +} + +/// A compressed bitmap of a page and the size, in points, it was taken at. +public struct BrowserPageSnapshotImage: Equatable, Sendable { + public var jpegData: Data + public var pointSize: CGSize + + public init(jpegData: Data, pointSize: CGSize) { + self.jpegData = jpegData + self.pointSize = pointSize + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/WebKit+BrowserFormState.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/WebKit+BrowserFormState.swift new file mode 100644 index 00000000000..5617b8dd7cf --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/PageRestoration/WebKit+BrowserFormState.swift @@ -0,0 +1,334 @@ +public import WebKit + +// The scripts that keep a discarded pane's unsaved form input. +// +// Both run in an isolated content world: they share the DOM with the page +// but not its JavaScript globals, so page script can neither read the +// reported values nor post fake reports. The observer is passive (capture +// phase listeners, no prototype or global changes) and main frame only. + +extension WKContentWorld { + /// Isolated world shared by the form-state observer, its message handler + /// and the restore call. + @MainActor + public static var browserFormState: WKContentWorld { + .world(name: browserFormStateName) + } + + /// Name shared by the content world and the script message handler. + fileprivate static let browserFormStateName = "cmuxFormState" +} + +extension WKUserScript { + /// Document-start observer. After input settles, and when the page is + /// hidden, it reports every control whose value differs from its default, + /// keyed by a locator the restore script can resolve again, and whether + /// the page holds typed input the restore cannot replay: a changed + /// password, payment or file field, a value past the size limits, or an + /// edit to rich text or a shadow-root control. Rich-text edits stay + /// counted until the document goes away. + @MainActor + public static func browserFormStateObserver() -> WKUserScript { + WKUserScript( + source: browserFormStateObserverSource, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + in: .browserFormState + ) + } + + private static let browserFormStateObserverSource = #""" + (() => { + try { + // Keep these transport caps in sync with BrowserFormStateSnapshot constants. + const MAX_FIELDS = 200; + const MAX_VALUE = 65536; + const EXCLUDED_TYPES = new Set(["password", "hidden", "file", "button", "submit", "reset", "image"]); + const NON_INPUT_TYPES = new Set(["hidden", "button", "submit", "reset", "image"]); + const isSensitiveAutocomplete = (raw) => String(raw || "").toLowerCase().split(/\s+/).some((token) => + token === "off" || token === "one-time-code" || token.startsWith("cc-") || token.endsWith("-password")); + const isEligible = (el) => { + if (el.disabled) return false; + if (el instanceof HTMLInputElement && EXCLUDED_TYPES.has(el.type)) return false; + if (isSensitiveAutocomplete(el.getAttribute("autocomplete"))) return false; + if (el.form && isSensitiveAutocomplete(el.form.getAttribute("autocomplete"))) return false; + return true; + }; + const keyFor = (el) => { + if (el.id) return "id:" + el.id; + if (el.name) { + const form = el.form; + const formIndex = form ? Array.prototype.indexOf.call(document.forms, form) : -1; + const scope = form ? form.elements : document.getElementsByName(el.name); + let index = 0; + for (const other of scope) { + if (other === el) break; + if (other.name === el.name) index += 1; + } + return "name:" + formIndex + ":" + el.name + ":" + index; + } + const parts = []; + let node = el; + while (node && node !== document.documentElement && node.parentElement) { + parts.push(node.tagName.toLowerCase() + ":" + Array.prototype.indexOf.call(node.parentElement.children, node)); + node = node.parentElement; + } + return "path:" + parts.reverse().join("/"); + }; + const selectState = (el) => { + const options = Array.from(el.options); + const selected = []; + let defaults = []; + options.forEach((option, index) => { + if (option.selected) selected.push(index); + if (option.defaultSelected) defaults.push(index); + }); + if (!el.multiple) { + if (defaults.length > 1) defaults = [defaults[defaults.length - 1]]; + if (defaults.length === 0 && el.size <= 1) { + const first = options.findIndex((option) => !option.disabled); + if (first >= 0) defaults = [first]; + } + } + return selected.join(",") === defaults.join(",") ? null : { s: selected }; + }; + const fieldState = (el) => { + if (el instanceof HTMLSelectElement) return selectState(el); + if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) { + return el.checked === el.defaultChecked ? null : { c: el.checked }; + } + if (el.value === el.defaultValue || el.value.length > MAX_VALUE) return null; + return { v: el.value }; + }; + const isChanged = (el) => { + if (el.disabled) return false; + if (el instanceof HTMLSelectElement) return selectState(el) !== null; + if (el instanceof HTMLInputElement) { + if (NON_INPUT_TYPES.has(el.type)) return false; + if (el.type === "file") return !!el.files && el.files.length > 0; + if (el.type === "checkbox" || el.type === "radio") return el.checked !== el.defaultChecked; + } + return el.value !== el.defaultValue; + }; + let editedUntrackedContent = false; + const collect = () => { + const fields = []; + const seen = new Set(); + let unrestorable = editedUntrackedContent; + for (const el of document.querySelectorAll("input, textarea, select")) { + if (!isChanged(el)) continue; + if (!isEligible(el) || fields.length >= MAX_FIELDS) { + unrestorable = true; + continue; + } + const state = fieldState(el); + const key = state ? keyFor(el) : null; + if (!state || seen.has(key)) { + unrestorable = true; + continue; + } + seen.add(key); + state.k = key; + fields.push(state); + } + return { fields, unrestorable }; + }; + let lastReported = JSON.stringify({ fields: [], unrestorable: false }); + let timer = null; + let unloading = false; + const flush = () => { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + if (unloading) return; + let state = { fields: [], unrestorable: editedUntrackedContent }; + try { state = collect(); } catch (_) {} + const serialized = JSON.stringify(state); + if (serialized === lastReported) return; + lastReported = serialized; + try { + window.webkit.messageHandlers["\#(WKContentWorld.browserFormStateName)"].postMessage({ + url: String(location.href), + fields: state.fields, + unrestorable: state.unrestorable + }); + } catch (_) {} + }; + const schedule = () => { + if (timer !== null) clearTimeout(timer); + timer = setTimeout(flush, 250); + }; + document.addEventListener("input", (event) => { + // The first composed path entry is the edited node even inside an + // open shadow root, which the form scan above cannot reach. + const origin = typeof event.composedPath === "function" ? event.composedPath()[0] : event.target; + const nativeControl = origin instanceof HTMLInputElement || origin instanceof HTMLTextAreaElement || origin instanceof HTMLSelectElement; + if (origin && (!nativeControl || origin.isContentEditable || (origin.getRootNode && origin.getRootNode() !== document))) { + editedUntrackedContent = true; + } + schedule(); + }, true); + document.addEventListener("change", schedule, true); + document.addEventListener("visibilitychange", () => { + if (document.visibilityState === "hidden") flush(); + }, true); + // A report sent while the document unloads can arrive after the next + // document commits and be taken for its input. WebKit keeps form + // values of pages navigated away from in their history items. + window.addEventListener("pagehide", () => { + unloading = true; + if (timer !== null) clearTimeout(timer); + timer = null; + }, true); + // A back/forward cache return commits natively, which clears the + // pane's copy of the input, so report it again. + window.addEventListener("pageshow", (event) => { + unloading = false; + if (event.persisted) { + lastReported = JSON.stringify({ fields: [], unrestorable: false }); + schedule(); + } + }, true); + } catch (_) {} + return true; + })(); + """# +} + +extension WKUserContentController { + /// Routes reports from ``WKUserScript/browserFormStateObserver()`` to + /// `handler`. + @MainActor + public func addBrowserFormStateHandler(_ handler: BrowserFormStateMessageHandler) { + add(handler, contentWorld: .browserFormState, name: WKContentWorld.browserFormStateName) + } + + @MainActor + public func removeBrowserFormStateHandler() { + removeScriptMessageHandler(forName: WKContentWorld.browserFormStateName, contentWorld: .browserFormState) + } +} + +extension WKWebView { + /// Refills the current document's controls from `formState`: only + /// controls the page has not changed itself, dispatching `input` and + /// `change` so frameworks see the values, and waiting a bounded time for + /// controls rendered later. + /// + /// - Parameter onFailure: Called on the main actor if the script fails. + @MainActor + public func restoreBrowserFormState( + _ formState: BrowserFormStateSnapshot, + onFailure: @escaping @MainActor @Sendable (any Error) -> Void + ) { + callAsyncJavaScript( + Self.browserFormStateRestoreFunctionBody, + arguments: [ + "fields": formState.restorePayload, + "timeoutMs": Self.browserFormStateRestoreTimeoutMilliseconds + ], + in: nil, + in: .browserFormState + ) { result in + guard case .failure(let error) = result else { return } + Task { @MainActor in + onFailure(error) + } + } + } + + /// How long the restore script waits for late-rendered controls, such as + /// a single-page app that builds its form after the document loads. + private static let browserFormStateRestoreTimeoutMilliseconds = 5_000 + + /// Body for `callAsyncJavaScript` with arguments `fields` (the snapshot's + /// ``BrowserFormStateSnapshot/restorePayload``) and `timeoutMs`. Fills + /// controls the page has not changed itself, dispatches `input` and + /// `change` so frameworks see the values, waits up to `timeoutMs` for + /// controls rendered later, and resolves to the number restored. + private static let browserFormStateRestoreFunctionBody = #""" + const pending = new Map(); + for (const field of fields) pending.set(field.k, field); + let restored = 0; + const findByKey = (key) => { + if (key.startsWith("id:")) return document.getElementById(key.slice(3)); + if (key.startsWith("name:")) { + const rest = key.slice(5); + const first = rest.indexOf(":"); + const last = rest.lastIndexOf(":"); + if (first < 0 || last <= first) return null; + const formIndex = Number(rest.slice(0, first)); + const name = rest.slice(first + 1, last); + const wanted = Number(rest.slice(last + 1)); + const form = formIndex >= 0 ? document.forms[formIndex] : null; + if (formIndex >= 0 && !form) return null; + const scope = form ? form.elements : document.getElementsByName(name); + let index = 0; + for (const el of scope) { + if (el.name !== name) continue; + if (index === wanted) return el; + index += 1; + } + return null; + } + if (key.startsWith("path:")) { + let node = document.documentElement; + for (const part of key.slice(5).split("/")) { + if (!part) continue; + const separator = part.lastIndexOf(":"); + const child = node ? node.children[Number(part.slice(separator + 1))] : null; + if (!child || child.tagName.toLowerCase() !== part.slice(0, separator)) return null; + node = child; + } + return node; + } + return null; + }; + const apply = (el, field) => { + if (el instanceof HTMLSelectElement) { + if (!Array.isArray(field.s)) return true; + const current = Array.from(el.options).flatMap((option, index) => option.selected ? [index] : []); + let defaults = Array.from(el.options).flatMap((option, index) => option.defaultSelected ? [index] : []); + if (!el.multiple && defaults.length === 0 && el.options.length > 0) defaults.push(0); + if (current.join(",") !== defaults.join(",")) return true; + if (field.s.some((index) => index < 0 || index >= el.options.length) || (!el.multiple && field.s.length > 1)) return true; + const wanted = new Set(field.s); + Array.from(el.options).forEach((option, index) => { option.selected = wanted.has(index); }); + } else if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) { + if (typeof field.c !== "boolean" || el.checked === field.c || el.checked !== el.defaultChecked) return true; + el.checked = field.c; + } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) { + if (el instanceof HTMLInputElement && (el.type === "password" || el.type === "file" || el.type === "hidden")) return true; + if (typeof field.v !== "string" || el.value === field.v || el.value !== el.defaultValue) return true; + el.value = field.v; + } else { + return false; + } + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + restored += 1; + return true; + }; + const applyPending = () => { + for (const [key, field] of pending) { + const el = findByKey(key); + if (el && apply(el, field)) pending.delete(key); + } + return pending.size === 0; + }; + if (applyPending()) return restored; + return await new Promise((resolve) => { + let timer = null; + let observer = null; + const finish = () => { + if (observer) observer.disconnect(); + if (timer !== null) clearTimeout(timer); + resolve(restored); + }; + observer = new MutationObserver(() => { if (applyPending()) finish(); }); + observer.observe(document.documentElement, { childList: true, subtree: true }); + timer = setTimeout(finish, timeoutMs); + }); + """# +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardBlockerSnapshot.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardBlockerSnapshot.swift new file mode 100644 index 00000000000..76ffbf74961 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardBlockerSnapshot.swift @@ -0,0 +1,96 @@ +/// How urgently a hidden pane's web content is being released. +public enum BrowserHiddenWebViewDiscardUrgency: Sendable { + /// Hidden time or the hidden memory budget. Anything a restore cannot + /// bring back keeps the pane. + case routine + /// The system is under memory pressure. State a restore cannot bring back, + /// such as typed input the form restore skips or a WebContent process that + /// died mid-load, no longer keeps the pane. Live activity still does. + case systemMemoryPressure + + public init(reason: String) { + self = reason == BrowserHiddenWebViewDiscardManager.systemMemoryPressureReason + ? .systemMemoryPressure + : .routine + } +} + +extension BrowserHiddenWebViewDiscardManager { + public struct BlockerSnapshot { + public let isClosing: Bool + public let isVisibleInUI: Bool + public let shouldRenderWebView: Bool + public let hasPendingRemoteNavigation: Bool + public let hasCurrentURL: Bool + public let isLoading: Bool + public let webViewIsLoading: Bool + public let hasActiveMainFrameProvisionalNavigation: Bool + public let hasRecoverableWebContentTermination: Bool + public let isDownloading: Bool + public let activeDownloadCount: Int + public let preferredDeveloperToolsVisible: Bool + public let isDeveloperToolsVisible: Bool + public let isElementFullscreenActive: Bool + public let isReactGrabActive: Bool + public var isDesignModeActive = false + public let isVisualAutomationCaptureActive: Bool + public let isMobileBrowserStreamActive: Bool + public let hasPopups: Bool + public let isCapturingMedia: Bool + public let isPlayingMedia: Bool + public var isPictureInPictureActive = false + /// The page holds typed input the form restore does not replay, such + /// as a password or a rich-text edit. + public var hasUnrestorableFormInput = false + + public init( + isClosing: Bool, + isVisibleInUI: Bool, + shouldRenderWebView: Bool, + hasPendingRemoteNavigation: Bool, + hasCurrentURL: Bool, + isLoading: Bool, + webViewIsLoading: Bool, + hasActiveMainFrameProvisionalNavigation: Bool, + hasRecoverableWebContentTermination: Bool = false, + isDownloading: Bool, + activeDownloadCount: Int, + preferredDeveloperToolsVisible: Bool, + isDeveloperToolsVisible: Bool, + isElementFullscreenActive: Bool, + isReactGrabActive: Bool, + isDesignModeActive: Bool = false, + isVisualAutomationCaptureActive: Bool, + isMobileBrowserStreamActive: Bool = false, + hasPopups: Bool, + isCapturingMedia: Bool, + isPlayingMedia: Bool, + isPictureInPictureActive: Bool = false, + hasUnrestorableFormInput: Bool = false + ) { + self.isClosing = isClosing + self.isVisibleInUI = isVisibleInUI + self.shouldRenderWebView = shouldRenderWebView + self.hasPendingRemoteNavigation = hasPendingRemoteNavigation + self.hasCurrentURL = hasCurrentURL + self.isLoading = isLoading + self.webViewIsLoading = webViewIsLoading + self.hasActiveMainFrameProvisionalNavigation = hasActiveMainFrameProvisionalNavigation + self.hasRecoverableWebContentTermination = hasRecoverableWebContentTermination + self.isDownloading = isDownloading + self.activeDownloadCount = activeDownloadCount + self.preferredDeveloperToolsVisible = preferredDeveloperToolsVisible + self.isDeveloperToolsVisible = isDeveloperToolsVisible + self.isElementFullscreenActive = isElementFullscreenActive + self.isReactGrabActive = isReactGrabActive + self.isDesignModeActive = isDesignModeActive + self.isVisualAutomationCaptureActive = isVisualAutomationCaptureActive + self.isMobileBrowserStreamActive = isMobileBrowserStreamActive + self.hasPopups = hasPopups + self.isCapturingMedia = isCapturingMedia + self.isPlayingMedia = isPlayingMedia + self.isPictureInPictureActive = isPictureInPictureActive + self.hasUnrestorableFormInput = hasUnrestorableFormInput + } + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardManager.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardManager.swift index be01df1fb94..dd7d349857b 100644 --- a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardManager.swift +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardManager.swift @@ -21,77 +21,8 @@ public protocol BrowserHiddenWebViewDiscardManagerDelegate: AnyObject { @MainActor public final class BrowserHiddenWebViewDiscardManager { - public static let systemMemoryPressureReason = "system_memory_pressure" - - public struct BlockerSnapshot { - public let isClosing: Bool - public let isVisibleInUI: Bool - public let shouldRenderWebView: Bool - public let hasPendingRemoteNavigation: Bool - public let hasCurrentURL: Bool - public let isLoading: Bool - public let webViewIsLoading: Bool - public let hasActiveMainFrameProvisionalNavigation: Bool - public let hasRecoverableWebContentTermination: Bool - public let isDownloading: Bool - public let activeDownloadCount: Int - public let preferredDeveloperToolsVisible: Bool - public let isDeveloperToolsVisible: Bool - public let isElementFullscreenActive: Bool - public let isReactGrabActive: Bool - public var isDesignModeActive = false - public let isVisualAutomationCaptureActive: Bool - public let isMobileBrowserStreamActive: Bool - public let hasPopups: Bool - public let isCapturingMedia: Bool - public let isPlayingMedia: Bool - - public init( - isClosing: Bool, - isVisibleInUI: Bool, - shouldRenderWebView: Bool, - hasPendingRemoteNavigation: Bool, - hasCurrentURL: Bool, - isLoading: Bool, - webViewIsLoading: Bool, - hasActiveMainFrameProvisionalNavigation: Bool, - hasRecoverableWebContentTermination: Bool = false, - isDownloading: Bool, - activeDownloadCount: Int, - preferredDeveloperToolsVisible: Bool, - isDeveloperToolsVisible: Bool, - isElementFullscreenActive: Bool, - isReactGrabActive: Bool, - isDesignModeActive: Bool = false, - isVisualAutomationCaptureActive: Bool, - isMobileBrowserStreamActive: Bool = false, - hasPopups: Bool, - isCapturingMedia: Bool, - isPlayingMedia: Bool - ) { - self.isClosing = isClosing - self.isVisibleInUI = isVisibleInUI - self.shouldRenderWebView = shouldRenderWebView - self.hasPendingRemoteNavigation = hasPendingRemoteNavigation - self.hasCurrentURL = hasCurrentURL - self.isLoading = isLoading - self.webViewIsLoading = webViewIsLoading - self.hasActiveMainFrameProvisionalNavigation = hasActiveMainFrameProvisionalNavigation - self.hasRecoverableWebContentTermination = hasRecoverableWebContentTermination - self.isDownloading = isDownloading - self.activeDownloadCount = activeDownloadCount - self.preferredDeveloperToolsVisible = preferredDeveloperToolsVisible - self.isDeveloperToolsVisible = isDeveloperToolsVisible - self.isElementFullscreenActive = isElementFullscreenActive - self.isReactGrabActive = isReactGrabActive - self.isDesignModeActive = isDesignModeActive - self.isVisualAutomationCaptureActive = isVisualAutomationCaptureActive - self.isMobileBrowserStreamActive = isMobileBrowserStreamActive - self.hasPopups = hasPopups - self.isCapturingMedia = isCapturingMedia - self.isPlayingMedia = isPlayingMedia - } - } + public nonisolated static let systemMemoryPressureReason = "system_memory_pressure" + public nonisolated static let memoryBudgetReason = "hidden_memory_budget" public weak var delegate: (any BrowserHiddenWebViewDiscardManagerDelegate)? @@ -120,22 +51,50 @@ public final class BrowserHiddenWebViewDiscardManager { public private(set) var lastRestoreReason: String? public private(set) var restoredSessionShouldRenderWebView: Bool? public private(set) var isRestoreNavigationPending: Bool = false + /// Whether the discarded page is a relaunched pane's first load, which + /// was deferred until the pane is shown rather than unloaded to save + /// memory. + public private(set) var isDeferredFirstLoad = false + + /// A per-pane pin that keeps the page live while hidden, even under + /// system memory pressure. + public var keepsPageActive = false { + didSet { + guard keepsPageActive != oldValue else { return } + delegate?.hiddenWebViewDiscardManagerPolicyDidChange(self, reason: "keep_active_changed") + } + } + + /// Whether a page unloaded to save memory waits for the user to restore + /// it instead of restoring when its pane is shown. + public var waitsForManualRestore: Bool { + isDiscardedForMemory && !isRestoreNavigationPending && !isDeferredFirstLoad + && !BrowserHiddenWebViewDiscardPolicy.autoRestoresUnloadedPages(defaults: policyDefaults) + } public var hasScheduledDiscard: Bool { discardTimer != nil } + /// Whether hidden web views may be discarded at all under the current + /// settings. Panes skip discard-only preparation when it is off. + public var isPolicyEnabled: Bool { + BrowserHiddenWebViewDiscardPolicy.isEnabled(defaults: policyDefaults) + } + public func blockers( for snapshot: BlockerSnapshot, now: Date = Date(), - allowingRecoverableWebContentTermination: Bool = false + urgency: BrowserHiddenWebViewDiscardUrgency = .routine ) -> [String] { var blockers: [String] = [] if !BrowserHiddenWebViewDiscardPolicy.isEnabled(defaults: policyDefaults) { blockers.append("policy_disabled") } + if keepsPageActive { blockers.append("keep_active") } if isSystemSleeping { blockers.append("system_sleeping") } - if snapshot.hasRecoverableWebContentTermination && !allowingRecoverableWebContentTermination { + let isUnderPressure = urgency == .systemMemoryPressure + if snapshot.hasRecoverableWebContentTermination && !isUnderPressure { blockers.append("webcontent_recovery") } if snapshot.isClosing { blockers.append("closing") } @@ -144,8 +103,7 @@ public final class BrowserHiddenWebViewDiscardManager { if !snapshot.shouldRenderWebView { blockers.append("not_rendered") } if snapshot.hasPendingRemoteNavigation { blockers.append("pending_remote_navigation") } if !snapshot.hasCurrentURL { blockers.append("no_url") } - let allowsRecoverableDiscard = snapshot.hasRecoverableWebContentTermination && - allowingRecoverableWebContentTermination + let allowsRecoverableDiscard = snapshot.hasRecoverableWebContentTermination && isUnderPressure if (snapshot.isLoading || snapshot.webViewIsLoading) && !allowsRecoverableDiscard { blockers.append("loading") } @@ -153,6 +111,8 @@ public final class BrowserHiddenWebViewDiscardManager { if snapshot.isDownloading || snapshot.activeDownloadCount != 0 { blockers.append("download") } if snapshot.isCapturingMedia { blockers.append("media_capture") } if snapshot.isPlayingMedia { blockers.append("media_playback") } + if snapshot.isPictureInPictureActive { blockers.append("picture_in_picture") } + if snapshot.hasUnrestorableFormInput && !isUnderPressure { blockers.append("form_input") } if snapshot.preferredDeveloperToolsVisible || snapshot.isDeveloperToolsVisible { blockers.append("developer_tools") } @@ -168,18 +128,32 @@ public final class BrowserHiddenWebViewDiscardManager { public func scheduleIfNeeded( reason: String, now: Date = Date(), - allowingRecoverableWebContentTermination: Bool = false + urgency: BrowserHiddenWebViewDiscardUrgency = .routine + ) { + // Under the memory budget, hidden time alone never discards a pane; + // the budget coordinator picks the pane hidden longest. + guard BrowserHiddenWebViewDiscardPolicy.mode(defaults: policyDefaults) == .timer else { + cancel() + return + } + armDiscardCountdown(reason: reason, now: now, urgency: urgency) + } + + /// Discards the pane once it has been hidden for the delay, counted from + /// when it was hidden or the system last woke, whichever is later. + private func armDiscardCountdown( + reason: String, + now: Date, + urgency: BrowserHiddenWebViewDiscardUrgency ) { scheduleGeneration &+= 1 discardTimer?.cancel() discardTimer = nil guard let delegate else { return } - guard blockers( - for: delegate.hiddenWebViewDiscardSnapshot, - now: now, - allowingRecoverableWebContentTermination: allowingRecoverableWebContentTermination - ).isEmpty else { return } + guard blockers(for: delegate.hiddenWebViewDiscardSnapshot, now: now, urgency: urgency).isEmpty else { + return + } let observedWebViewInstanceID = delegate.hiddenWebViewDiscardWebViewInstanceID let generation = scheduleGeneration @@ -218,27 +192,24 @@ public final class BrowserHiddenWebViewDiscardManager { @discardableResult public func requestImmediateDiscardIfSafe(reason: String, now: Date = Date()) -> Bool { guard let delegate else { return false } - let allowsRecoverableWebContentTermination = reason == Self.systemMemoryPressureReason - guard blockers( - for: delegate.hiddenWebViewDiscardSnapshot, - now: now, - allowingRecoverableWebContentTermination: allowsRecoverableWebContentTermination - ).isEmpty else { return false } + let urgency = BrowserHiddenWebViewDiscardUrgency(reason: reason) + if urgency != .systemMemoryPressure, + BrowserHiddenWebViewDiscardPolicy.mode(defaults: policyDefaults) != .timer { + return false + } + guard blockers(for: delegate.hiddenWebViewDiscardSnapshot, now: now, urgency: urgency).isEmpty else { + return false + } + // A deferred pressure discard arms a countdown in either mode. Routine + // rescheduling may replace it; the monitor asks again on every sample + // while pressure lasts. guard delegate.hiddenWebViewDiscardHiddenAt != nil else { - scheduleIfNeeded( - reason: reason, - now: now, - allowingRecoverableWebContentTermination: allowsRecoverableWebContentTermination - ) + armDiscardCountdown(reason: reason, now: now, urgency: urgency) return false } // Memory pressure bypasses the hidden-duration delay, not the WebKit post-wake crash guard. guard !isInPostWakeDiscardDelay(now: now) else { - scheduleIfNeeded( - reason: reason, - now: now, - allowingRecoverableWebContentTermination: allowsRecoverableWebContentTermination - ) + armDiscardCountdown(reason: reason, now: now, urgency: urgency) return false } @@ -249,6 +220,29 @@ public final class BrowserHiddenWebViewDiscardManager { return true } + /// Whether the memory budget may discard the pane now: nothing blocks a + /// discard, and the pane has been hidden for the delay since it was + /// hidden or the system last woke, whichever is later. + public func isEligibleForMemoryBudgetDiscard(now: Date = Date()) -> Bool { + guard let delegate, let hiddenAt = delegate.hiddenWebViewDiscardHiddenAt else { return false } + guard blockers(for: delegate.hiddenWebViewDiscardSnapshot, now: now).isEmpty else { return false } + let effectiveHiddenAt = lastSystemWakeAt.map { max(hiddenAt, $0) } ?? hiddenAt + let hiddenDelay = BrowserHiddenWebViewDiscardPolicy.hiddenDelay(defaults: policyDefaults) + return now.timeIntervalSince(effectiveHiddenAt) >= hiddenDelay + } + + /// Discards the pane to bring hidden web content back under the memory + /// budget, if it is still eligible. + /// + /// - Returns: Whether the discard was requested. + @discardableResult + public func requestMemoryBudgetDiscard(now: Date = Date()) -> Bool { + guard let delegate, isEligibleForMemoryBudgetDiscard(now: now) else { return false } + cancel() + delegate.hiddenWebViewDiscardManagerDidRequestDiscard(self, reason: Self.memoryBudgetReason) + return true + } + public func cancel() { scheduleGeneration &+= 1 discardTimer?.cancel() @@ -321,9 +315,10 @@ public final class BrowserHiddenWebViewDiscardManager { } } - public func markDiscarded(reason: String, now: Date) { + public func markDiscarded(reason: String, now: Date, isDeferredFirstLoad: Bool = false) { isDiscardedForMemory = true isRestoreNavigationPending = false + self.isDeferredFirstLoad = isDeferredFirstLoad discardedAt = now lastDiscardReason = reason updateRestoredSessionRenderIntent(true) @@ -384,6 +379,7 @@ public final class BrowserHiddenWebViewDiscardManager { guard isDiscardedForMemory else { return false } isDiscardedForMemory = false isRestoreNavigationPending = false + isDeferredFirstLoad = false discardedAt = nil lastRestoreReason = reason updateRestoredSessionRenderIntent(nil) @@ -394,6 +390,7 @@ public final class BrowserHiddenWebViewDiscardManager { cancel() isDiscardedForMemory = false isRestoreNavigationPending = false + isDeferredFirstLoad = false discardedAt = nil lastDiscardReason = nil lastRestoreReason = nil diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardPolicy.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardPolicy.swift index 2a322d3596c..4fc8c10abc9 100644 --- a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardPolicy.swift +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewDiscardPolicy.swift @@ -1,3 +1,4 @@ +public import CmuxSettings public import Foundation /// lint:allow namespace-type: moved unchanged from the app target, where it was an internal static namespace; reshaping it is a separate change from this package move. @@ -5,6 +6,9 @@ public enum BrowserHiddenWebViewDiscardPolicy { public struct ResolvedPolicy: Equatable { public let isEnabled: Bool public let hiddenDelay: TimeInterval + public let mode: BrowserHiddenWebViewDiscardMode + public let memoryBudgetMB: Int + public let autoRestoresUnloadedPages: Bool } public static let enabledKey = "browserHiddenWebViewDiscardEnabled" @@ -13,6 +17,14 @@ public enum BrowserHiddenWebViewDiscardPolicy { public static let defaultHiddenDelay: TimeInterval = 300 static let minimumHiddenDelay: TimeInterval = 0 public static let maximumHiddenDelay: TimeInterval = 3600 + public static let modeKey = "browserHiddenWebViewDiscardMode" + public static let defaultMode: BrowserHiddenWebViewDiscardMode = .memoryBudget + public static let memoryBudgetKey = "browserHiddenWebViewMemoryBudgetMB" + public static let defaultMemoryBudgetMB = 2048 + public static let minimumMemoryBudgetMB = 256 + public static let maximumMemoryBudgetMB = 65536 + public static let autoRestoreKey = "browserAutoRestoreUnloadedPages" + public static let defaultAutoRestore = true public static var isEnabled: Bool { isEnabled(defaults: .standard) @@ -25,7 +37,10 @@ public enum BrowserHiddenWebViewDiscardPolicy { public static func resolved(defaults: UserDefaults = .standard) -> ResolvedPolicy { ResolvedPolicy( isEnabled: isEnabled(defaults: defaults), - hiddenDelay: hiddenDelay(defaults: defaults) + hiddenDelay: hiddenDelay(defaults: defaults), + mode: mode(defaults: defaults), + memoryBudgetMB: memoryBudgetMB(defaults: defaults), + autoRestoresUnloadedPages: autoRestoresUnloadedPages(defaults: defaults) ) } @@ -72,4 +87,45 @@ public enum BrowserHiddenWebViewDiscardPolicy { } return resolvedValue } + + /// The discard mode, from `CMUX_BROWSER_HIDDEN_WEBVIEW_DISCARD_MODE` or + /// the stored setting. Unknown values fall back to the memory budget. + public static func mode(defaults: UserDefaults) -> BrowserHiddenWebViewDiscardMode { + let rawValue = ProcessInfo.processInfo.environment["CMUX_BROWSER_HIDDEN_WEBVIEW_DISCARD_MODE"]? + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + if let rawValue, let mode = BrowserHiddenWebViewDiscardMode(rawValue: rawValue) { + return mode + } + return defaults.string(forKey: modeKey).flatMap(BrowserHiddenWebViewDiscardMode.init(rawValue:)) + ?? defaultMode + } + + /// Returns `value` when it is a supported budget, in megabytes. + public static func resolvedMemoryBudgetMB(_ value: Int) -> Int? { + (minimumMemoryBudgetMB...maximumMemoryBudgetMB).contains(value) ? value : nil + } + + /// The hidden web content memory budget in megabytes, from + /// `CMUX_BROWSER_HIDDEN_WEBVIEW_MEMORY_BUDGET_MB` or the stored setting. + public static func memoryBudgetMB(defaults: UserDefaults) -> Int { + let rawValue = ProcessInfo.processInfo.environment["CMUX_BROWSER_HIDDEN_WEBVIEW_MEMORY_BUDGET_MB"]? + .trimmingCharacters(in: .whitespacesAndNewlines) + if let rawValue, let value = Int(rawValue), let resolvedValue = resolvedMemoryBudgetMB(value) { + return resolvedValue + } + guard defaults.object(forKey: memoryBudgetKey) != nil, + let storedValue = resolvedMemoryBudgetMB(defaults.integer(forKey: memoryBudgetKey)) else { + return defaultMemoryBudgetMB + } + return storedValue + } + + /// Whether an unloaded page restores as soon as its pane is shown. When + /// off, the pane shows a placeholder until the user restores it + /// (https://github.com/manaflow-ai/cmux/issues/9561). + public static func autoRestoresUnloadedPages(defaults: UserDefaults) -> Bool { + guard defaults.object(forKey: autoRestoreKey) != nil else { return defaultAutoRestore } + return defaults.bool(forKey: autoRestoreKey) + } } diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewMemoryBudgetPlanner.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewMemoryBudgetPlanner.swift new file mode 100644 index 00000000000..3d534a715f2 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserHiddenWebViewMemoryBudgetPlanner.swift @@ -0,0 +1,94 @@ +public import Foundation + +/// Picks the hidden browser panes to discard so the memory held by hidden web +/// content fits a budget, starting with the pane hidden longest, like a +/// Chrome tab discard (https://github.com/manaflow-ai/cmux/issues/15069). +/// +/// Panes can share a WebContent process, which frees its memory only once no +/// pane uses it, so the process is the unit of eviction. A process that also +/// backs a visible pane is not hidden memory and is never counted. +public struct BrowserHiddenWebViewMemoryBudgetPlanner: Sendable { + public struct Pane: Equatable, Sendable { + public let id: UUID + /// The pane's live WebContent process, or nil when it has none. + public let processID: Int? + public let isVisible: Bool + public let hiddenAt: Date? + /// Whether the pane may be discarded now. + public let isEvictable: Bool + + public init(id: UUID, processID: Int?, isVisible: Bool, hiddenAt: Date?, isEvictable: Bool) { + self.id = id + self.processID = processID + self.isVisible = isVisible + self.hiddenAt = hiddenAt + self.isEvictable = isEvictable + } + } + + public struct Plan: Equatable, Sendable { + /// Physical memory held by processes that back only hidden panes. + public let hiddenFootprintBytes: UInt64 + /// The same after the planned discards. + public let remainingHiddenFootprintBytes: UInt64 + /// Panes to discard, hidden longest first. + public let panesToDiscard: [UUID] + } + + public let budgetBytes: UInt64 + + public init(budgetBytes: UInt64) { + self.budgetBytes = budgetBytes + } + + public init(budgetMB: Int) { + let megabytes = max(0, budgetMB) + let budgetBytes = megabytes > Int.max / 1_048_576 + ? UInt64.max + : UInt64(megabytes) * 1_048_576 + self.init(budgetBytes: budgetBytes) + } + + /// - Parameter footprintBytes: The physical footprint of a process, or + /// nil when it cannot be measured. Called only for hidden processes. + public func plan(panes: [Pane], footprintBytes: (Int) -> UInt64?) -> Plan { + let visibleProcessIDs = Set(panes.filter(\.isVisible).compactMap(\.processID)) + var hiddenPanesByProcess: [Int: [Pane]] = [:] + for pane in panes where !pane.isVisible { + guard let processID = pane.processID, !visibleProcessIDs.contains(processID) else { continue } + hiddenPanesByProcess[processID, default: []].append(pane) + } + var footprints: [Int: UInt64] = [:] + for processID in hiddenPanesByProcess.keys { + footprints[processID] = footprintBytes(processID) + } + let hiddenFootprintBytes = footprints.values.reduce(0, +) + + // A process is as old as its most recently hidden pane. + let evictableProcesses = hiddenPanesByProcess + .filter { processID, panes in + (footprints[processID] ?? 0) > 0 && panes.allSatisfy(\.isEvictable) + } + .map { processID, panes in + ( + processID: processID, + hiddenAt: panes.map { $0.hiddenAt ?? .distantFuture }.max() ?? .distantFuture, + paneIDs: panes.map(\.id) + ) + } + .sorted { ($0.hiddenAt, $0.processID) < ($1.hiddenAt, $1.processID) } + + var remainingBytes = hiddenFootprintBytes + var panesToDiscard: [UUID] = [] + for process in evictableProcesses { + guard remainingBytes > budgetBytes else { break } + panesToDiscard += process.paneIDs + remainingBytes -= footprints[process.processID] ?? 0 + } + return Plan( + hiddenFootprintBytes: hiddenFootprintBytes, + remainingHiddenFootprintBytes: remainingBytes, + panesToDiscard: panesToDiscard + ) + } +} diff --git a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebViewSupport.swift b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebViewSupport.swift index 73467cec748..f627bb01a55 100644 --- a/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebViewSupport.swift +++ b/Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebViewSupport.swift @@ -32,6 +32,29 @@ extension WKWebView { return true } + /// Whether the page is capturing the camera, microphone, a display or + /// system audio, counting muted capture as capture. + public var cmuxIsCapturingMedia: Bool { + cameraCaptureState != .none + || microphoneCaptureState != .none + || cmuxCaptureState(selectorName: "_displayCaptureState") + || cmuxCaptureState(selectorName: "_systemAudioCaptureState") + } + + /// Reads a capture state WebKit exposes only as SPI, such as the display + /// capture behind screen sharing. Zero means none. + private func cmuxCaptureState(selectorName: String) -> Bool { + let selector = NSSelectorFromString(selectorName) + guard responds(to: selector), + let implementation = method(for: selector) else { + return true + } + + typealias CaptureStateFunction = @convention(c) (AnyObject, Selector) -> Int + let function = unsafeBitCast(implementation, to: CaptureStateFunction.self) + return function(self, selector) != 0 + } + public var cmuxIsElementFullscreenActiveOrTransitioning: Bool { switch fullscreenState { case .notInFullscreen: diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserDiscardRestoreStrategyTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserDiscardRestoreStrategyTests.swift new file mode 100644 index 00000000000..a705734d651 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserDiscardRestoreStrategyTests.swift @@ -0,0 +1,116 @@ +import Foundation +import Testing +@testable import CmuxBrowser + +struct BrowserDiscardRestoreStrategyTests { + private let pageURL = URL(string: "https://example.com/app#inbox")! + private let state = Data([0x01, 0x02, 0x03]) + + private func capture(anchor: URL?, state: Data?) -> BrowserPageStateCapture { + BrowserPageStateCapture( + interactionState: state, + documentURL: anchor, + anchorURL: anchor, + coversNavigationHistory: true, + containsFormSubmission: false + ) + } + + @Test("A capture anchored at the restore URL restores its session state") + func restoresInteractionStateAtAnchor() { + let strategy = BrowserDiscardRestoreStrategy.resolve( + restoreURL: pageURL, + capture: capture(anchor: pageURL, state: state) + ) + #expect(strategy == .restoreInteractionState(state)) + } + + @Test("Without a capture the restore URL is replayed") + func replaysWithoutCapture() { + #expect(BrowserDiscardRestoreStrategy.resolve(restoreURL: pageURL, capture: nil) == .replayURL(pageURL)) + } + + @Test("A capture without session state replays the URL") + func replaysEmptyState() { + #expect( + BrowserDiscardRestoreStrategy.resolve(restoreURL: pageURL, capture: capture(anchor: pageURL, state: nil)) + == .replayURL(pageURL) + ) + #expect( + BrowserDiscardRestoreStrategy.resolve(restoreURL: pageURL, capture: capture(anchor: pageURL, state: Data())) + == .replayURL(pageURL) + ) + } + + @Test("A pane that moved to another URL since the discard replays that URL") + func replaysWhenAnchorDiffers() { + let otherURL = URL(string: "https://example.com/other")! + #expect( + BrowserDiscardRestoreStrategy.resolve(restoreURL: otherURL, capture: capture(anchor: pageURL, state: state)) + == .replayURL(otherURL) + ) + } + + /// Assigning session state for a page shown as a form submission result + /// makes WebKit send the form again, so such a page loads by URL. + @Test("A capture whose page came from a form submission replays the URL") + func replaysFormSubmissionDocument() { + let submitted = BrowserPageStateCapture( + interactionState: state, + documentURL: pageURL, + anchorURL: pageURL, + coversNavigationHistory: true, + containsFormSubmission: true, + documentHasFormSubmission: true + ) + #expect(BrowserDiscardRestoreStrategy.resolve(restoreURL: pageURL, capture: submitted) == .replayURL(pageURL)) + + var earlierSubmission = submitted + earlierSubmission.documentHasFormSubmission = false + #expect( + BrowserDiscardRestoreStrategy.resolve(restoreURL: pageURL, capture: earlierSubmission) + == .restoreInteractionState(state) + ) + } + + @Test( + "Reload, remote proxy, cloud routing, crash recovery and insecure HTTP replay the URL", + arguments: [ + BrowserDiscardRestoreStrategy.Conditions(isExplicitReload: true), + BrowserDiscardRestoreStrategy.Conditions(usesRemoteWorkspaceProxy: true), + BrowserDiscardRestoreStrategy.Conditions(usesCloudAccessRouting: true), + BrowserDiscardRestoreStrategy.Conditions(hasRecoverableWebContentTermination: true), + BrowserDiscardRestoreStrategy.Conditions(requiresInsecureHTTPConsent: true) + ] + ) + func forcedReplay(conditions: BrowserDiscardRestoreStrategy.Conditions) { + #expect( + BrowserDiscardRestoreStrategy.resolve( + restoreURL: pageURL, + capture: capture(anchor: pageURL, state: state), + conditions: conditions + ) == .replayURL(pageURL) + ) + } + + @Test("Web and local file documents restore session state; internal documents do not") + func restorableSchemes() { + #expect(BrowserDiscardRestoreStrategy.canRestoreSessionState(for: URL(string: "http://example.com/"))) + #expect(BrowserDiscardRestoreStrategy.canRestoreSessionState(for: URL(string: "HTTPS://example.com/"))) + #expect(BrowserDiscardRestoreStrategy.canRestoreSessionState(for: URL(fileURLWithPath: "/tmp/a.html"))) + #expect(!BrowserDiscardRestoreStrategy.canRestoreSessionState(for: URL(string: "cmux-diff-viewer://t/index"))) + #expect(!BrowserDiscardRestoreStrategy.canRestoreSessionState(for: URL(string: "about:blank"))) + #expect(!BrowserDiscardRestoreStrategy.canRestoreSessionState(for: nil)) + + let fileURL = URL(fileURLWithPath: "/tmp/a.html") + #expect( + BrowserDiscardRestoreStrategy.resolve(restoreURL: fileURL, capture: capture(anchor: fileURL, state: state)) + == .restoreInteractionState(state) + ) + let internalURL = URL(string: "cmux-diff-viewer://t/index")! + #expect( + BrowserDiscardRestoreStrategy.resolve(restoreURL: internalURL, capture: capture(anchor: internalURL, state: state)) + == .replayURL(internalURL) + ) + } +} diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserPageRestorationStateTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserPageRestorationStateTests.swift new file mode 100644 index 00000000000..c0f34d5a9d2 --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserPageRestorationStateTests.swift @@ -0,0 +1,326 @@ +import CoreGraphics +import Foundation +import Testing +@testable import CmuxBrowser + +@MainActor +struct BrowserPageRestorationStateTests { + private let pageURL = URL(string: "https://example.com/compose")! + private let sessionState = Data([0x0A, 0x0B]) + + private func form(_ url: URL, value: String = "draft") -> BrowserFormStateSnapshot { + BrowserFormStateSnapshot(documentURL: url, fields: [.init(key: "id:body", value: value)]) + } + + private func image(_ byte: UInt8) -> BrowserPageSnapshotImage { + BrowserPageSnapshotImage(jpegData: Data([byte]), pointSize: CGSize(width: 10, height: 20)) + } + + private func discard(_ restoration: BrowserPageRestorationState, covers: Bool = true) { + restoration.recordDiscard( + interactionState: sessionState, + documentURL: pageURL, + anchorURL: pageURL, + coversNavigationHistory: covers + ) + } + + @Test("A discard captures form input, snapshot and taint, and clears live state") + func discardCapturesLiveState() throws { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + let token = restoration.beginHiddenSnapshot() + restoration.completeHiddenSnapshot(token: token, image: image(1)) + + discard(restoration) + + let capture = try #require(restoration.discardedCapture) + #expect(capture.interactionState == sessionState) + #expect(capture.anchorURL == pageURL) + #expect(capture.formState == form(pageURL)) + #expect(capture.snapshot == image(1)) + #expect(capture.containsFormSubmission) + #expect(restoration.liveFormState == nil) + #expect(!restoration.liveContainsFormSubmission) + } + + @Test("Input reported for another origin is not captured") + func discardDropsForeignInput() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(URL(string: "https://other.example/")!)) + discard(restoration) + #expect(restoration.discardedCapture?.formState == nil) + } + + @Test("An empty report clears earlier input") + func emptyReportClearsInput() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + restoration.recordLiveFormState(BrowserFormStateSnapshot(documentURL: pageURL, fields: [])) + #expect(restoration.liveFormState == nil) + } + + @Test("A report of only unrestorable input is kept until the page clears it") + func unrestorableOnlyReportIsKept() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(BrowserFormStateSnapshot(documentURL: pageURL, fields: [], hasUnrestorableInput: true)) + #expect(restoration.hasUnrestorableLiveInput) + + restoration.recordLiveFormState(BrowserFormStateSnapshot(documentURL: pageURL, fields: [])) + #expect(!restoration.hasUnrestorableLiveInput) + #expect(restoration.liveFormState == nil) + } + + @Test("A new document forgets the previous document's unrestorable input") + func commitClearsUnrestorableInput() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(BrowserFormStateSnapshot(documentURL: pageURL, fields: [], hasUnrestorableInput: true)) + restoration.noteDocumentCommitted(isDiscardRestoreCommit: false) + #expect(!restoration.hasUnrestorableLiveInput) + } + + @Test("A snapshot finishing after the discard attaches to its capture") + func lateSnapshotAttaches() { + let restoration = BrowserPageRestorationState() + let token = restoration.beginHiddenSnapshot() + discard(restoration) + #expect(restoration.discardedCapture?.snapshot == nil) + + restoration.completeHiddenSnapshot(token: UUID(), image: image(9)) + #expect(restoration.discardedCapture?.snapshot == nil) + restoration.completeHiddenSnapshot(token: token, image: image(2)) + #expect(restoration.discardedCapture?.snapshot == image(2)) + } + + @Test("A snapshot for a pane shown again is dropped") + func cancelledSnapshotIsDropped() { + let restoration = BrowserPageRestorationState() + let token = restoration.beginHiddenSnapshot() + restoration.cancelHiddenSnapshot() + restoration.completeHiddenSnapshot(token: token, image: image(3)) + discard(restoration) + #expect(restoration.discardedCapture?.snapshot == nil) + } + + @Test("Discarding a web view whose restore never committed keeps the first capture") + func secondDiscardKeepsFirstCapture() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + discard(restoration) + restoration.noteRestoreStarted(.interactionState) + + restoration.recordDiscard( + interactionState: nil, + documentURL: nil, + anchorURL: pageURL, + coversNavigationHistory: false + ) + + #expect(restoration.discardedCapture?.interactionState == sessionState) + #expect(restoration.discardedCapture?.formState == form(pageURL)) + #expect(restoration.inFlightRestore == nil) + } + + @Test("A session-state restore commit brings back input and the form-submission taint") + func interactionStateCommit() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + discard(restoration) + restoration.noteRestoreStarted(.interactionState) + + let commit = restoration.noteDocumentCommitted(isDiscardRestoreCommit: true) + + #expect(commit == .init(method: .interactionState, coversNavigationHistory: true)) + #expect(restoration.discardedCapture == nil) + #expect(restoration.liveContainsFormSubmission) + #expect(restoration.takePendingFormRestore(for: pageURL) == form(pageURL)) + #expect(restoration.takePendingFormRestore(for: pageURL) == nil) + } + + @Test("A URL replay commit starts a fresh history without the taint") + func urlReplayCommit() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + discard(restoration, covers: false) + restoration.noteRestoreStarted(.urlReplay) + + let commit = restoration.noteDocumentCommitted(isDiscardRestoreCommit: true) + + #expect(commit == .init(method: .urlReplay, coversNavigationHistory: false)) + #expect(!restoration.liveContainsFormSubmission) + #expect(restoration.takePendingFormRestore(for: pageURL) == form(pageURL)) + } + + @Test("A commit elsewhere drops the capture and its input") + func unrelatedCommitDropsCapture() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + discard(restoration) + restoration.noteRestoreStarted(.interactionState) + + #expect(restoration.noteDocumentCommitted(isDiscardRestoreCommit: false) == nil) + #expect(restoration.discardedCapture == nil) + #expect(restoration.takePendingFormRestore(for: pageURL) == nil) + } + + @Test("Pending input is never applied to another origin") + func pendingInputStaysOnOrigin() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + discard(restoration) + restoration.noteRestoreStarted(.urlReplay) + restoration.noteDocumentCommitted(isDiscardRestoreCommit: true) + + #expect(restoration.takePendingFormRestore(for: URL(string: "https://login.example/")) == nil) + #expect(restoration.takePendingFormRestore(for: pageURL) == nil) + } + + @Test("An explicit reload drops the capture") + func dropCapture() { + let restoration = BrowserPageRestorationState() + discard(restoration) + restoration.noteRestoreStarted(.interactionState) + restoration.dropCapture() + #expect(restoration.discardedCapture == nil) + #expect(restoration.inFlightRestore == nil) + restoration.noteRestoreStarted(.urlReplay) + #expect(restoration.inFlightRestore == nil) + } + + @Test("Saved session state seeds a persistable capture") + func seedFromSessionState() { + let restoration = BrowserPageRestorationState() + restoration.seedFromSessionState(sessionState, documentURL: pageURL, anchorURL: pageURL) + #expect(restoration.discardedCapture?.anchorURL == pageURL) + #expect(restoration.persistableDiscardedInteractionState() == sessionState) + + let empty = BrowserPageRestorationState() + empty.seedFromSessionState(Data(), documentURL: pageURL, anchorURL: pageURL) + #expect(empty.discardedCapture == nil) + } + + @Test("A tainted capture is not persisted") + func taintedCaptureIsNotPersisted() { + let restoration = BrowserPageRestorationState() + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + discard(restoration) + #expect(restoration.persistableDiscardedInteractionState() == nil) + } + + @Test("A navigation started after the restore is not treated as the restore") + func laterNavigationSupersedesRestore() { + let restoration = BrowserPageRestorationState() + restoration.recordLiveFormState(form(pageURL)) + discard(restoration) + restoration.noteNavigationStarted() + restoration.noteRestoreStarted(.interactionState) + #expect(restoration.inFlightRestore == .interactionState) + + restoration.noteNavigationStarted() + + #expect(restoration.noteDocumentCommitted(isDiscardRestoreCommit: true) == nil) + #expect(restoration.takePendingFormRestore(for: pageURL) == nil) + } + + @Test("A page shown as a form submission result is marked in the capture") + func submittedDocumentIsMarked() throws { + let restoration = BrowserPageRestorationState() + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + restoration.noteDocumentCommitted(isDiscardRestoreCommit: false) + discard(restoration) + + let capture = try #require(restoration.discardedCapture) + #expect(capture.documentHasFormSubmission) + #expect(capture.containsFormSubmission) + } + + @Test("A submission redirected to a GET page leaves that page unmarked") + func redirectedSubmissionIsNotMarked() throws { + let restoration = BrowserPageRestorationState() + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: false) + restoration.noteDocumentCommitted(isDiscardRestoreCommit: false) + discard(restoration) + + let capture = try #require(restoration.discardedCapture) + #expect(!capture.documentHasFormSubmission) + #expect(capture.containsFormSubmission) + } + + @Test("A form submitted in a frame marks the page that holds the frame") + func subframeSubmissionMarksPage() throws { + let restoration = BrowserPageRestorationState() + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: false) + restoration.noteDocumentCommitted(isDiscardRestoreCommit: false) + restoration.noteNavigationRequest(targetFrameIsMainFrame: false, isFormSubmission: false) + restoration.noteNavigationRequest(targetFrameIsMainFrame: false, isFormSubmission: true) + discard(restoration) + + let capture = try #require(restoration.discardedCapture) + #expect(capture.documentHasFormSubmission) + #expect(capture.containsFormSubmission) + } + + @Test("A request that opens a new window leaves this pane's marks alone") + func newWindowRequestIsIgnored() throws { + let restoration = BrowserPageRestorationState() + restoration.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + restoration.noteNavigationRequest(targetFrameIsMainFrame: nil, isFormSubmission: false) + restoration.noteDocumentCommitted(isDiscardRestoreCommit: false) + discard(restoration) + let submitted = try #require(restoration.discardedCapture) + #expect(submitted.documentHasFormSubmission) + + let plain = BrowserPageRestorationState() + plain.noteNavigationRequest(targetFrameIsMainFrame: nil, isFormSubmission: true) + plain.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: false) + plain.noteDocumentCommitted(isDiscardRestoreCommit: false) + discard(plain) + let capture = try #require(plain.discardedCapture) + #expect(!capture.documentHasFormSubmission) + #expect(!capture.containsFormSubmission) + } + + @Test("Only a commit moves the form submission mark to another page") + func markFollowsCommits() throws { + let navigatedAway = BrowserPageRestorationState() + navigatedAway.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + navigatedAway.noteDocumentCommitted(isDiscardRestoreCommit: false) + navigatedAway.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: false) + navigatedAway.noteDocumentCommitted(isDiscardRestoreCommit: false) + discard(navigatedAway) + let away = try #require(navigatedAway.discardedCapture) + #expect(!away.documentHasFormSubmission) + #expect(away.containsFormSubmission) + + let stayed = BrowserPageRestorationState() + stayed.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: true) + stayed.noteDocumentCommitted(isDiscardRestoreCommit: false) + stayed.noteNavigationRequest(targetFrameIsMainFrame: true, isFormSubmission: false) + discard(stayed) + let stay = try #require(stayed.discardedCapture) + #expect(stay.documentHasFormSubmission) + } + + @Test("Reactivating reports whether assigned session state holds the history") + func reactivationReportsRestoredHistory() { + let restoring = BrowserPageRestorationState() + discard(restoring) + restoring.noteRestoreStarted(.interactionState) + #expect(restoring.noteReactivatedWithoutRestore()) + #expect(restoring.discardedCapture == nil) + + let partial = BrowserPageRestorationState() + discard(partial, covers: false) + partial.noteRestoreStarted(.interactionState) + #expect(!partial.noteReactivatedWithoutRestore()) + + let idle = BrowserPageRestorationState() + discard(idle) + #expect(!idle.noteReactivatedWithoutRestore()) + #expect(idle.discardedCapture == nil) + } +} diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserPageStateCaptureTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserPageStateCaptureTests.swift new file mode 100644 index 00000000000..6224dec593f --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/PageRestoration/BrowserPageStateCaptureTests.swift @@ -0,0 +1,156 @@ +import Foundation +import Testing +@testable import CmuxBrowser + +struct BrowserPageStateCaptureTests { + private let state = Data(repeating: 0xAB, count: 64) + + private func capture( + state: Data?, + coversNavigationHistory: Bool = true, + containsFormSubmission: Bool = false + ) -> BrowserPageStateCapture { + BrowserPageStateCapture( + interactionState: state, + documentURL: URL(string: "https://example.com/"), + coversNavigationHistory: coversNavigationHistory, + containsFormSubmission: containsFormSubmission + ) + } + + @Test("State covering the pane's history without form submissions persists") + func persistsCleanState() { + #expect(capture(state: state).persistableInteractionState() == state) + } + + @Test("State holding a form submission never persists") + func dropsFormSubmission() { + #expect(capture(state: state, containsFormSubmission: true).persistableInteractionState() == nil) + } + + @Test("State that replays restored URL history does not persist") + func dropsPartialHistory() { + #expect(capture(state: state, coversNavigationHistory: false).persistableInteractionState() == nil) + } + + @Test("Missing, empty and oversized state does not persist") + func dropsEmptyAndOversizedState() { + #expect(capture(state: nil).persistableInteractionState() == nil) + #expect(capture(state: Data()).persistableInteractionState() == nil) + #expect(capture(state: state).persistableInteractionState(maxBytes: 63) == nil) + #expect(capture(state: state).persistableInteractionState(maxBytes: 64) == state) + } +} + +struct BrowserFormStateSnapshotTests { + @Test("Parses text, checkbox and select fields from an observer report") + func parsesReport() throws { + let body: [String: Any] = [ + "url": "https://example.com/compose", + "fields": [ + ["k": "id:subject", "v": "Hello"], + ["k": "name:0:urgent:0", "c": true], + ["k": "path:body:0/select:2", "s": [NSNumber(value: 1), NSNumber(value: 3)]] + ] as [[String: Any]] + ] + let snapshot = try #require(BrowserFormStateSnapshot(messageBody: body)) + #expect(snapshot.documentURL == URL(string: "https://example.com/compose")) + #expect(snapshot.fields == [ + .init(key: "id:subject", value: "Hello"), + .init(key: "name:0:urgent:0", isChecked: true), + .init(key: "path:body:0/select:2", selectedOptionIndexes: [1, 3]) + ]) + } + + @Test("Rejects malformed reports and skips malformed fields") + func rejectsMalformed() throws { + #expect(BrowserFormStateSnapshot(messageBody: "nope") == nil) + #expect(BrowserFormStateSnapshot(messageBody: ["fields": [] as [Any]] as [String: Any]) == nil) + let body: [String: Any] = [ + "url": "https://example.com/", + "fields": [["k": "", "v": "x"], ["v": "no key"], ["k": "id:a"], "junk", ["k": "id:b", "v": "kept"]] as [Any] + ] + let snapshot = try #require(BrowserFormStateSnapshot(messageBody: body)) + #expect(snapshot.fields == [.init(key: "id:b", value: "kept")]) + } + + @Test("Caps field count and value length") + func capsFields() throws { + let oversized = String(repeating: "x", count: BrowserFormStateSnapshot.maxValueLength + 1) + var fields: [[String: Any]] = [["k": "id:big", "v": oversized]] + for index in 0..<(BrowserFormStateSnapshot.maxFieldCount + 10) { + fields.append(["k": "id:f\(index)", "v": "\(index)"]) + } + let snapshot = try #require( + BrowserFormStateSnapshot(messageBody: ["url": "https://example.com/", "fields": fields] as [String: Any]) + ) + #expect(snapshot.fields.count == BrowserFormStateSnapshot.maxFieldCount) + #expect(snapshot.fields.first?.key == "id:f0") + #expect(snapshot.hasUnrestorableInput) + } + + @Test("Input the observer or the caps leave out counts as unrestorable") + func unrestorableInput() throws { + let plain = try #require( + BrowserFormStateSnapshot(messageBody: ["url": "https://example.com/", "fields": [["k": "id:a", "v": "x"]]] as [String: Any]) + ) + #expect(!plain.hasUnrestorableInput) + + let flagged = try #require( + BrowserFormStateSnapshot(messageBody: ["url": "https://example.com/", "fields": [] as [Any], "unrestorable": true] as [String: Any]) + ) + #expect(flagged.isEmpty) + #expect(flagged.hasUnrestorableInput) + + let oversized = String(repeating: "x", count: BrowserFormStateSnapshot.maxValueLength + 1) + let capped = try #require( + BrowserFormStateSnapshot(messageBody: ["url": "https://example.com/", "fields": [["k": "id:big", "v": oversized]]] as [String: Any]) + ) + #expect(capped.isEmpty) + #expect(capped.hasUnrestorableInput) + } + + @Test("Input stays on its origin; file URLs must be the same file") + func originMatching() { + let snapshot = BrowserFormStateSnapshot( + documentURL: URL(string: "https://example.com/a?x=1#top")!, + fields: [.init(key: "id:a", value: "v")] + ) + #expect(snapshot.sharesOrigin(with: URL(string: "https://EXAMPLE.com/b"))) + #expect(!snapshot.sharesOrigin(with: URL(string: "https://example.com:8443/a"))) + #expect(!snapshot.sharesOrigin(with: URL(string: "http://example.com/a"))) + #expect(!snapshot.sharesOrigin(with: URL(string: "https://evil.example/a"))) + #expect(!snapshot.sharesOrigin(with: nil)) + + let fileSnapshot = BrowserFormStateSnapshot( + documentURL: URL(fileURLWithPath: "/tmp/a.html"), + fields: [.init(key: "id:a", value: "v")] + ) + #expect(fileSnapshot.sharesOrigin(with: URL(string: "file:///tmp/a.html#section"))) + #expect(!fileSnapshot.sharesOrigin(with: URL(fileURLWithPath: "/tmp/b.html"))) + + let networkFile = URL(string: "file://server-a/tmp/a.html")! + let otherNetworkFile = URL(string: "file://server-b/tmp/a.html")! + let networkFileFragment = URL(string: "file://server-a/tmp/a.html#section")! + #expect(BrowserFormStateSnapshot.isSameDocument(networkFile, networkFileFragment)) + #expect(!BrowserFormStateSnapshot.isSameDocument(networkFile, otherNetworkFile)) + } + + @Test("Restore payload mirrors the report shape") + func restorePayload() { + let snapshot = BrowserFormStateSnapshot( + documentURL: URL(string: "https://example.com/")!, + fields: [ + .init(key: "id:a", value: "text"), + .init(key: "id:b", isChecked: false), + .init(key: "id:c", selectedOptionIndexes: [2]) + ] + ) + let payload = snapshot.restorePayload + #expect(payload.count == 3) + #expect(payload[0]["k"] as? String == "id:a") + #expect(payload[0]["v"] as? String == "text") + #expect(payload[1]["c"] as? Bool == false) + #expect(payload[2]["s"] as? [Int] == [2]) + } +} diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewDiscardBlockerTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewDiscardBlockerTests.swift new file mode 100644 index 00000000000..b1c7a6ac27a --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewDiscardBlockerTests.swift @@ -0,0 +1,195 @@ +import Foundation +import Testing +@testable import CmuxBrowser + +/// Coverage for https://github.com/manaflow-ai/cmux/issues/15069: typed input +/// a restore cannot replay, Picture in Picture and the per-pane keep-active pin +/// each keep a hidden pane's page. System memory pressure overrides only the +/// typed input, which a restore loses but which is not live activity. +@MainActor +struct BrowserHiddenWebViewDiscardBlockerTests { + @Test("Unrestorable typed input blocks a budget discard but not a pressure discard") + func unrestorableInputYieldsToPressure() { + let now = Date() + let (manager, delegate, cleanup) = makeManager(hiddenAt: now.addingTimeInterval(-3600)) + defer { cleanup() } + delegate.snapshot = BlockerDelegate.makeSnapshot(hasUnrestorableFormInput: true) + + #expect(manager.blockers(for: delegate.snapshot, now: now) == ["form_input"]) + #expect(!manager.requestMemoryBudgetDiscard(now: now)) + #expect(delegate.discardRequests.isEmpty) + + #expect(manager.blockers(for: delegate.snapshot, now: now, urgency: .systemMemoryPressure).isEmpty) + let pressureReason = BrowserHiddenWebViewDiscardManager.systemMemoryPressureReason + #expect(manager.requestImmediateDiscardIfSafe(reason: pressureReason, now: now)) + #expect(delegate.discardRequests == [pressureReason]) + } + + @Test("Picture in Picture blocks both budget and pressure discards") + func pictureInPictureBlocksEveryDiscard() { + let now = Date() + let (manager, delegate, cleanup) = makeManager(hiddenAt: now.addingTimeInterval(-3600)) + defer { cleanup() } + delegate.snapshot = BlockerDelegate.makeSnapshot(isPictureInPictureActive: true) + + #expect(manager.blockers(for: delegate.snapshot, now: now) == ["picture_in_picture"]) + #expect(!manager.requestMemoryBudgetDiscard(now: now)) + let pressureReason = BrowserHiddenWebViewDiscardManager.systemMemoryPressureReason + #expect(!manager.requestImmediateDiscardIfSafe(reason: pressureReason, now: now)) + #expect(delegate.discardRequests.isEmpty) + } + + @Test("A keep-active pin blocks both budget and pressure discards") + func keepActivePinBlocksEveryDiscard() { + let now = Date() + let (manager, delegate, cleanup) = makeManager(hiddenAt: now.addingTimeInterval(-3600)) + defer { cleanup() } + manager.keepsPageActive = true + + #expect(manager.blockers(for: delegate.snapshot, now: now) == ["keep_active"]) + #expect(!manager.isEligibleForMemoryBudgetDiscard(now: now)) + #expect(!manager.requestMemoryBudgetDiscard(now: now)) + let pressureReason = BrowserHiddenWebViewDiscardManager.systemMemoryPressureReason + #expect(!manager.requestImmediateDiscardIfSafe(reason: pressureReason, now: now)) + #expect(delegate.discardRequests.isEmpty) + + manager.keepsPageActive = false + #expect(manager.requestMemoryBudgetDiscard(now: now)) + #expect(delegate.discardRequests == [BrowserHiddenWebViewDiscardManager.memoryBudgetReason]) + } + + @Test("Toggling the keep-active pin re-evaluates the pane's discard policy once per change") + func keepActivePinNotifiesPolicyChange() { + let (manager, delegate, cleanup) = makeManager(hiddenAt: Date()) + defer { cleanup() } + + manager.keepsPageActive = true + manager.keepsPageActive = true + manager.keepsPageActive = false + + #expect(delegate.policyChanges == ["keep_active_changed", "keep_active_changed"]) + } + + @Test("Pressure still keeps a pane that is playing media or capturing it") + func pressureRespectsLiveMedia() { + let now = Date() + let (manager, _, cleanup) = makeManager(hiddenAt: now.addingTimeInterval(-3600)) + defer { cleanup() } + + let playing = BlockerDelegate.makeSnapshot(isPlayingMedia: true) + #expect(manager.blockers(for: playing, now: now, urgency: .systemMemoryPressure) == ["media_playback"]) + let capturing = BlockerDelegate.makeSnapshot(isCapturingMedia: true) + #expect(manager.blockers(for: capturing, now: now, urgency: .systemMemoryPressure) == ["media_capture"]) + } + + @Test("Only the system memory pressure reason is urgent") + func urgencyFromReason() { + #expect(BrowserHiddenWebViewDiscardUrgency(reason: BrowserHiddenWebViewDiscardManager.systemMemoryPressureReason) == .systemMemoryPressure) + #expect(BrowserHiddenWebViewDiscardUrgency(reason: BrowserHiddenWebViewDiscardManager.memoryBudgetReason) == .routine) + #expect(BrowserHiddenWebViewDiscardUrgency(reason: "hidden_timer") == .routine) + } + + private func makeManager(hiddenAt: Date?) -> (BrowserHiddenWebViewDiscardManager, BlockerDelegate, () -> Void) { + let suiteName = "cmux-hidden-webview-blockers-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defaults.removePersistentDomain(forName: suiteName) + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = BlockerDelegate(hiddenAt: hiddenAt) + manager.delegate = delegate + return (manager, delegate, { defaults.removePersistentDomain(forName: suiteName) }) + } +} + +/// Coverage for how the media-playback hook's per-frame reports fold into the +/// page's playback and Picture in Picture state. +struct BrowserMediaPlaybackFramesTests { + @Test("Picture in Picture stays active while its video is paused") + func pausedPictureInPictureStaysActive() { + var frames = BrowserMediaPlaybackFrames() + frames.apply(BrowserMediaPlaybackReport(frameID: "main", isPlaying: true, isAudible: true, isPictureInPicture: true)) + #expect(frames.isPlaying && frames.isAudible && frames.isPictureInPictureActive) + + frames.apply(BrowserMediaPlaybackReport(frameID: "main", isPlaying: false, isAudible: true, isPictureInPicture: true)) + #expect(!frames.isPlaying) + #expect(!frames.isAudible) + #expect(frames.isPictureInPictureActive) + + frames.apply(BrowserMediaPlaybackReport(frameID: "main", isPlaying: false, isAudible: false)) + #expect(!frames.isPictureInPictureActive) + } + + @Test("A frame's report replaces only that frame's state") + func framesAggregateIndependently() { + var frames = BrowserMediaPlaybackFrames() + frames.apply(BrowserMediaPlaybackReport(frameID: "main", isPlaying: true, isAudible: false)) + frames.apply(BrowserMediaPlaybackReport(frameID: "iframe", isPlaying: false, isAudible: false, isPictureInPicture: true)) + #expect(frames.isPlaying) + #expect(!frames.isAudible) + #expect(frames.isPictureInPictureActive) + + frames.apply(BrowserMediaPlaybackReport(frameID: "main", isPlaying: false, isAudible: false)) + #expect(!frames.isPlaying) + #expect(frames.isPictureInPictureActive) + } +} + +@MainActor +private final class BlockerDelegate: BrowserHiddenWebViewDiscardManagerDelegate { + var snapshot = makeSnapshot() + var hiddenAt: Date? + let webViewInstanceID = UUID() + private(set) var discardRequests: [String] = [] + private(set) var policyChanges: [String] = [] + + init(hiddenAt: Date?) { + self.hiddenAt = hiddenAt + } + + static func makeSnapshot( + isCapturingMedia: Bool = false, + isPlayingMedia: Bool = false, + isPictureInPictureActive: Bool = false, + hasUnrestorableFormInput: Bool = false + ) -> BrowserHiddenWebViewDiscardManager.BlockerSnapshot { + BrowserHiddenWebViewDiscardManager.BlockerSnapshot( + isClosing: false, + isVisibleInUI: false, + shouldRenderWebView: true, + hasPendingRemoteNavigation: false, + hasCurrentURL: true, + isLoading: false, + webViewIsLoading: false, + hasActiveMainFrameProvisionalNavigation: false, + isDownloading: false, + activeDownloadCount: 0, + preferredDeveloperToolsVisible: false, + isDeveloperToolsVisible: false, + isElementFullscreenActive: false, + isReactGrabActive: false, + isVisualAutomationCaptureActive: false, + hasPopups: false, + isCapturingMedia: isCapturingMedia, + isPlayingMedia: isPlayingMedia, + isPictureInPictureActive: isPictureInPictureActive, + hasUnrestorableFormInput: hasUnrestorableFormInput + ) + } + + var hiddenWebViewDiscardSnapshot: BrowserHiddenWebViewDiscardManager.BlockerSnapshot { snapshot } + var hiddenWebViewDiscardHiddenAt: Date? { hiddenAt } + var hiddenWebViewDiscardWebViewInstanceID: UUID { webViewInstanceID } + + func hiddenWebViewDiscardManagerDidRequestDiscard( + _ manager: BrowserHiddenWebViewDiscardManager, + reason: String + ) { + discardRequests.append(reason) + } + + func hiddenWebViewDiscardManagerPolicyDidChange( + _ manager: BrowserHiddenWebViewDiscardManager, + reason: String + ) { + policyChanges.append(reason) + } +} diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewManualRestoreTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewManualRestoreTests.swift new file mode 100644 index 00000000000..7db33c482fc --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewManualRestoreTests.swift @@ -0,0 +1,89 @@ +import Foundation +import Testing +@testable import CmuxBrowser + +/// Coverage for the `browser.autoRestoreUnloadedPages` setting +/// (https://github.com/manaflow-ai/cmux/issues/9561 and +/// https://github.com/manaflow-ai/cmux/issues/15069): with it off, a page +/// unloaded to save memory waits for the user instead of restoring when its +/// pane is shown. +@MainActor +struct BrowserHiddenWebViewManualRestoreTests { + @Test("Unloaded pages restore automatically unless the setting is off") + func policyDefaultsToAutomaticRestore() { + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + #expect(BrowserHiddenWebViewDiscardPolicy.autoRestoresUnloadedPages(defaults: defaults)) + #expect(BrowserHiddenWebViewDiscardPolicy.resolved(defaults: defaults).autoRestoresUnloadedPages) + + defaults.set(false, forKey: BrowserHiddenWebViewDiscardPolicy.autoRestoreKey) + #expect(!BrowserHiddenWebViewDiscardPolicy.autoRestoresUnloadedPages(defaults: defaults)) + #expect(!BrowserHiddenWebViewDiscardPolicy.resolved(defaults: defaults).autoRestoresUnloadedPages) + } + + @Test("A page unloaded for memory waits for the user only when automatic restore is off") + func discardedPageWaitsWhenAutomaticRestoreIsOff() { + let (automaticDefaults, automaticCleanup) = makeDefaults() + defer { automaticCleanup() } + let automatic = BrowserHiddenWebViewDiscardManager(policyDefaults: automaticDefaults) + automatic.markDiscarded(reason: BrowserHiddenWebViewDiscardManager.memoryBudgetReason, now: Date()) + #expect(!automatic.waitsForManualRestore) + + let (manual, manualCleanup) = makeManualManager() + defer { manualCleanup() } + #expect(!manual.waitsForManualRestore) + manual.markDiscarded(reason: BrowserHiddenWebViewDiscardManager.memoryBudgetReason, now: Date()) + #expect(manual.waitsForManualRestore) + } + + @Test("A restore the user started no longer waits") + func startedRestoreStopsWaiting() { + let (manager, cleanup) = makeManualManager() + defer { cleanup() } + manager.markDiscarded(reason: BrowserHiddenWebViewDiscardManager.systemMemoryPressureReason, now: Date()) + #expect(manager.waitsForManualRestore) + + manager.noteRestoreNavigationStarted(reason: "manual_restore") + #expect(!manager.waitsForManualRestore) + + #expect(manager.noteRestoreNavigationCommitted(reason: "manual_restore")) + #expect(!manager.waitsForManualRestore) + } + + @Test("A relaunched pane's deferred first load never waits for the user") + func deferredFirstLoadDoesNotWait() { + let (manager, cleanup) = makeManualManager() + defer { cleanup() } + manager.markDiscarded(reason: "session_restore", now: Date(), isDeferredFirstLoad: true) + #expect(manager.isDeferredFirstLoad) + #expect(!manager.waitsForManualRestore) + + manager.resetMetadata() + #expect(!manager.isDeferredFirstLoad) + manager.markDiscarded(reason: BrowserHiddenWebViewDiscardManager.memoryBudgetReason, now: Date()) + #expect(manager.waitsForManualRestore) + } + + @Test("Clearing the discard also clears the deferred first load") + func clearingDiscardResetsDeferredFirstLoad() { + let (manager, cleanup) = makeManualManager() + defer { cleanup() } + manager.markDiscarded(reason: "session_restore", now: Date(), isDeferredFirstLoad: true) + #expect(manager.clearDiscardState(reason: "test")) + #expect(!manager.isDeferredFirstLoad) + #expect(!manager.waitsForManualRestore) + } + + private func makeManualManager() -> (BrowserHiddenWebViewDiscardManager, () -> Void) { + let (defaults, cleanup) = makeDefaults() + defaults.set(false, forKey: BrowserHiddenWebViewDiscardPolicy.autoRestoreKey) + return (BrowserHiddenWebViewDiscardManager(policyDefaults: defaults), cleanup) + } + + private func makeDefaults() -> (UserDefaults, () -> Void) { + let suiteName = "cmux-hidden-webview-manual-restore-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defaults.removePersistentDomain(forName: suiteName) + return (defaults, { defaults.removePersistentDomain(forName: suiteName) }) + } +} diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewMemoryBudgetPlannerTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewMemoryBudgetPlannerTests.swift new file mode 100644 index 00000000000..12b78923f9f --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewMemoryBudgetPlannerTests.swift @@ -0,0 +1,106 @@ +import Foundation +import Testing +@testable import CmuxBrowser + +struct BrowserHiddenWebViewMemoryBudgetPlannerTests { + private typealias Pane = BrowserHiddenWebViewMemoryBudgetPlanner.Pane + private static let megabyte: UInt64 = 1024 * 1024 + private let now = Date() + + @Test("Hidden panes under the budget are all kept") + func keepsHiddenPanesUnderBudget() { + let panes = [hidden(processID: 1, minutesAgo: 30), hidden(processID: 2, minutesAgo: 20)] + + let plan = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 1024) + .plan(panes: panes) { _ in 400 * Self.megabyte } + + #expect(plan.hiddenFootprintBytes == 800 * Self.megabyte) + #expect(plan.panesToDiscard.isEmpty) + } + + @Test("Over the budget, the pane hidden longest goes first until hidden memory fits") + func discardsOldestHiddenFirstUntilUnderBudget() { + let newest = hidden(processID: 1, minutesAgo: 10) + let oldest = hidden(processID: 2, minutesAgo: 40) + let middle = hidden(processID: 3, minutesAgo: 25) + + let plan = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 1024) + .plan(panes: [newest, oldest, middle]) { _ in 600 * Self.megabyte } + + #expect(plan.panesToDiscard == [oldest.id, middle.id]) + #expect(plan.remainingHiddenFootprintBytes == 600 * Self.megabyte) + } + + @Test("A process that also backs a visible pane is neither counted nor discarded") + func ignoresProcessSharedWithVisiblePane() { + let visible = Pane(id: UUID(), processID: 1, isVisible: true, hiddenAt: nil, isEvictable: false) + let sharedHidden = hidden(processID: 1, minutesAgo: 60) + let ownHidden = hidden(processID: 2, minutesAgo: 5) + var measured: [Int] = [] + + let plan = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 256) + .plan(panes: [visible, sharedHidden, ownHidden]) { processID in + measured.append(processID) + return 600 * Self.megabyte + } + + #expect(measured == [2]) + #expect(plan.hiddenFootprintBytes == 600 * Self.megabyte) + #expect(plan.panesToDiscard == [ownHidden.id]) + } + + @Test("Hidden panes that share a process are discarded together, dated by the newest") + func discardsSharedHiddenProcessAsUnit() { + let sharedOld = hidden(processID: 1, minutesAgo: 60) + let sharedRecent = hidden(processID: 1, minutesAgo: 5) + let alone = hidden(processID: 2, minutesAgo: 30) + + let plan = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 700) + .plan(panes: [sharedOld, sharedRecent, alone]) { _ in 400 * Self.megabyte } + + #expect(plan.panesToDiscard == [alone.id]) + + let tighter = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 256) + .plan(panes: [sharedOld, sharedRecent, alone]) { _ in 400 * Self.megabyte } + + #expect(tighter.panesToDiscard == [alone.id, sharedOld.id, sharedRecent.id]) + } + + @Test("A pane that may not be discarded keeps its whole process") + func blockedPaneProtectsItsProcess() { + let blocked = hidden(processID: 1, minutesAgo: 60, isEvictable: false) + let sharesBlocked = hidden(processID: 1, minutesAgo: 50) + let other = hidden(processID: 2, minutesAgo: 10) + + let plan = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 256) + .plan(panes: [blocked, sharesBlocked, other]) { _ in 400 * Self.megabyte } + + #expect(plan.panesToDiscard == [other.id]) + #expect(plan.remainingHiddenFootprintBytes == 400 * Self.megabyte) + } + + @Test("Panes without a measurable process are never discarded") + func skipsUnmeasuredProcesses() { + let discarded = Pane(id: UUID(), processID: nil, isVisible: false, hiddenAt: now, isEvictable: true) + let unmeasured = hidden(processID: 1, minutesAgo: 60) + let measured = hidden(processID: 2, minutesAgo: 10) + + let plan = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: 256) + .plan(panes: [discarded, unmeasured, measured]) { processID in + processID == 2 ? 400 * Self.megabyte : nil + } + + #expect(plan.hiddenFootprintBytes == 400 * Self.megabyte) + #expect(plan.panesToDiscard == [measured.id]) + } + + private func hidden(processID: Int, minutesAgo: Double, isEvictable: Bool = true) -> Pane { + Pane( + id: UUID(), + processID: processID, + isVisible: false, + hiddenAt: now.addingTimeInterval(-minutesAgo * 60), + isEvictable: isEvictable + ) + } +} diff --git a/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewMemoryBudgetTests.swift b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewMemoryBudgetTests.swift new file mode 100644 index 00000000000..b05fe35f20d --- /dev/null +++ b/Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/WebView/BrowserHiddenWebViewMemoryBudgetTests.swift @@ -0,0 +1,192 @@ +import Foundation +import Testing +@testable import CmuxBrowser + +/// Regression coverage for https://github.com/manaflow-ai/cmux/issues/15069: +/// by default a hidden pane keeps its page until hidden web content exceeds +/// the memory budget, like a Chrome tab discard. A fixed hidden-time timer +/// must be opt-in, so an idle pane hidden past the delay is not discarded +/// just because time passed. +@MainActor +struct BrowserHiddenWebViewMemoryBudgetTests { + @Test("The default policy does not discard an idle hidden pane on a timer") + func defaultPolicyDoesNotDiscardOnTimer() { + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + defaults.set(true, forKey: BrowserHiddenWebViewDiscardPolicy.enabledKey) + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: Date().addingTimeInterval(-3600)) + manager.delegate = delegate + + manager.scheduleIfNeeded(reason: "test.hidden") + + #expect(delegate.discardRequestCount == 0) + #expect(!manager.hasScheduledDiscard) + } + + @Test("Timer mode still discards a pane hidden past the delay") + func timerModeDiscardsPaneHiddenPastDelay() { + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + defaults.set("timer", forKey: BrowserHiddenWebViewDiscardPolicy.modeKey) + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: Date().addingTimeInterval(-3600)) + manager.delegate = delegate + + manager.scheduleIfNeeded(reason: "test.hidden") + + #expect(delegate.discardRequests == ["test.hidden"]) + } + + @Test("Timer mode arms a countdown for a pane hidden less than the delay") + func timerModeArmsCountdownWithinDelay() { + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + defaults.set("timer", forKey: BrowserHiddenWebViewDiscardPolicy.modeKey) + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: Date()) + manager.delegate = delegate + + manager.scheduleIfNeeded(reason: "test.hidden") + defer { manager.cancel() } + + #expect(manager.hasScheduledDiscard) + #expect(delegate.discardRequests.isEmpty) + } + + @Test("The budget may discard a pane only once it has been hidden for the delay") + func budgetEligibilityRequiresHiddenDelay() { + let now = Date() + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: now.addingTimeInterval(-60)) + manager.delegate = delegate + #expect(!manager.isEligibleForMemoryBudgetDiscard(now: now)) + + delegate.hiddenAt = now.addingTimeInterval(-BrowserHiddenWebViewDiscardPolicy.defaultHiddenDelay) + #expect(manager.isEligibleForMemoryBudgetDiscard(now: now)) + + delegate.hiddenAt = nil + #expect(!manager.isEligibleForMemoryBudgetDiscard(now: now)) + } + + @Test("The budget never discards a pane a blocker protects") + func budgetEligibilityRespectsBlockers() { + let now = Date() + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: now.addingTimeInterval(-3600)) + delegate.snapshot = DiscardDelegate.makeSnapshot(isPlayingMedia: true) + manager.delegate = delegate + + #expect(!manager.isEligibleForMemoryBudgetDiscard(now: now)) + #expect(!manager.requestMemoryBudgetDiscard(now: now)) + #expect(delegate.discardRequests.isEmpty) + } + + @Test("The budget waits the delay again after the system wakes") + func budgetEligibilityRestartsAfterWake() { + let now = Date() + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: now.addingTimeInterval(-3600)) + manager.delegate = delegate + + manager.noteSystemDidWake(now: now.addingTimeInterval(-60)) + + #expect(!manager.isEligibleForMemoryBudgetDiscard(now: now)) + } + + @Test("A budget discard reports the memory budget reason") + func budgetDiscardReportsReason() { + let now = Date() + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + let manager = BrowserHiddenWebViewDiscardManager(policyDefaults: defaults) + let delegate = DiscardDelegate(hiddenAt: now.addingTimeInterval(-3600)) + manager.delegate = delegate + + #expect(manager.requestMemoryBudgetDiscard(now: now)) + #expect(delegate.discardRequests == [BrowserHiddenWebViewDiscardManager.memoryBudgetReason]) + } + + @Test("Mode and budget settings fall back to their defaults when unset or invalid") + func policyResolvesModeAndBudget() { + let (defaults, cleanup) = makeDefaults() + defer { cleanup() } + #expect(BrowserHiddenWebViewDiscardPolicy.mode(defaults: defaults) == .memoryBudget) + #expect(BrowserHiddenWebViewDiscardPolicy.memoryBudgetMB(defaults: defaults) == 2048) + + defaults.set("timer", forKey: BrowserHiddenWebViewDiscardPolicy.modeKey) + defaults.set(4096, forKey: BrowserHiddenWebViewDiscardPolicy.memoryBudgetKey) + #expect(BrowserHiddenWebViewDiscardPolicy.mode(defaults: defaults) == .timer) + #expect(BrowserHiddenWebViewDiscardPolicy.memoryBudgetMB(defaults: defaults) == 4096) + + defaults.set("sometimes", forKey: BrowserHiddenWebViewDiscardPolicy.modeKey) + defaults.set(100, forKey: BrowserHiddenWebViewDiscardPolicy.memoryBudgetKey) + #expect(BrowserHiddenWebViewDiscardPolicy.mode(defaults: defaults) == .memoryBudget) + #expect(BrowserHiddenWebViewDiscardPolicy.memoryBudgetMB(defaults: defaults) == 2048) + } + + private func makeDefaults() -> (UserDefaults, () -> Void) { + let suiteName = "cmux-hidden-webview-budget-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defaults.removePersistentDomain(forName: suiteName) + return (defaults, { defaults.removePersistentDomain(forName: suiteName) }) + } +} + +@MainActor +private final class DiscardDelegate: BrowserHiddenWebViewDiscardManagerDelegate { + var snapshot = makeSnapshot() + var hiddenAt: Date? + let webViewInstanceID = UUID() + private(set) var discardRequests: [String] = [] + var discardRequestCount: Int { discardRequests.count } + + init(hiddenAt: Date?) { + self.hiddenAt = hiddenAt + } + + static func makeSnapshot(isPlayingMedia: Bool = false) -> BrowserHiddenWebViewDiscardManager.BlockerSnapshot { + BrowserHiddenWebViewDiscardManager.BlockerSnapshot( + isClosing: false, + isVisibleInUI: false, + shouldRenderWebView: true, + hasPendingRemoteNavigation: false, + hasCurrentURL: true, + isLoading: false, + webViewIsLoading: false, + hasActiveMainFrameProvisionalNavigation: false, + isDownloading: false, + activeDownloadCount: 0, + preferredDeveloperToolsVisible: false, + isDeveloperToolsVisible: false, + isElementFullscreenActive: false, + isReactGrabActive: false, + isVisualAutomationCaptureActive: false, + hasPopups: false, + isCapturingMedia: false, + isPlayingMedia: isPlayingMedia + ) + } + + var hiddenWebViewDiscardSnapshot: BrowserHiddenWebViewDiscardManager.BlockerSnapshot { snapshot } + var hiddenWebViewDiscardHiddenAt: Date? { hiddenAt } + var hiddenWebViewDiscardWebViewInstanceID: UUID { webViewInstanceID } + + func hiddenWebViewDiscardManagerDidRequestDiscard( + _ manager: BrowserHiddenWebViewDiscardManager, + reason: String + ) { + discardRequests.append(reason) + } + + func hiddenWebViewDiscardManagerPolicyDidChange( + _ manager: BrowserHiddenWebViewDiscardManager, + reason: String + ) {} +} diff --git a/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Network/TeamsClient.swift b/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Network/TeamsClient.swift index 50997bf71db..2841c16ca51 100644 --- a/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Network/TeamsClient.swift +++ b/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Network/TeamsClient.swift @@ -153,19 +153,20 @@ public actor TeamsClient { static let decoder: JSONDecoder = { let decoder = JSONDecoder() + // Every team date is written by `Date.toISOString()` on the web, which + // always emits milliseconds, and `.iso8601` rejects fractional seconds. + // Accept both shapes, the way `VMClient` already does. The format + // styles are Sendable, unlike `ISO8601DateFormatter`. + let withFractions = Date.ISO8601FormatStyle(includingFractionalSeconds: true) + let wholeSeconds = Date.ISO8601FormatStyle() decoder.dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let raw = try container.decode(String.self) - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - if let date = formatter.date(from: raw) { - return date - } - formatter.formatOptions = [.withInternetDateTime] - guard let date = formatter.date(from: raw) else { + guard let date = (try? Date(raw, strategy: withFractions)) + ?? (try? Date(raw, strategy: wholeSeconds)) else { throw DecodingError.dataCorruptedError( in: container, - debugDescription: "Expected an ISO8601 date string." + debugDescription: "Expected an ISO 8601 team date, got \"\(raw)\"" ) } return date diff --git a/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/TeamsClientModelsTests.swift b/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/TeamsClientModelsTests.swift index aa5a4842c5c..83a83828eda 100644 --- a/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/TeamsClientModelsTests.swift +++ b/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/TeamsClientModelsTests.swift @@ -51,6 +51,52 @@ struct TeamsClientModelsTests { #expect(detail.openSeats == nil) } + @Test("decodes the millisecond timestamps the team API actually sends") + func decodesFractionalSecondTimestamps() throws { + // Every team date on the wire is written by `Date.toISOString()`, which + // always emits milliseconds. Whole-second strings have to keep decoding + // too, so that a trimmed or hand-built payload is not a hard failure. + let detail = try TeamsClient.decoder.decode(CloudTeamDetail.self, from: Data(detailJSON.utf8)) + #expect(detail.invitations.first?.expiresAt == Date(timeIntervalSince1970: 1_791_201_600)) + #expect(detail.links.first?.createdAt == Date(timeIntervalSince1970: 1_790_596_800)) + + let wholeSeconds = detailJSON.replacingOccurrences(of: ".000Z", with: "Z") + // Guard the replacement itself: if a fixture date stops ending in + // ".000Z" this test would quietly stop covering the fallback. + #expect(wholeSeconds != detailJSON) + let trimmed = try TeamsClient.decoder.decode(CloudTeamDetail.self, from: Data(wholeSeconds.utf8)) + #expect(trimmed.invitations.first?.expiresAt == detail.invitations.first?.expiresAt) + #expect(trimmed.links.first?.createdAt == detail.links.first?.createdAt) + + // Every fixture date sits on a whole second, so a decoder that parses + // the fraction and then discards it would pass everything above. Pin a + // real sub-second value, with a tolerance because the parse lands on + // 1791201600.1230001 rather than an exact binary .123. + let subSecond = detailJSON.replacingOccurrences(of: "12:00:00.000Z", with: "12:00:00.123Z") + #expect(subSecond != detailJSON) + let precise = try TeamsClient.decoder.decode(CloudTeamDetail.self, from: Data(subSecond.utf8)) + let expiresAt = try #require(precise.invitations.first?.expiresAt) + #expect(abs(expiresAt.timeIntervalSince1970 - 1_791_201_600.123) < 0.000_1) + + // `links[0].expiresAt` is null in the fixture, so the optional date is + // only ever exercised as an absent value. A link created with an expiry + // is the normal case, so decode that shape too. + let datedLink = detailJSON.replacingOccurrences( + of: "\"expiresAt\": null", + with: "\"expiresAt\": \"2026-10-12T12:00:00.000Z\"" + ) + #expect(datedLink != detailJSON) + let withExpiry = try TeamsClient.decoder.decode(CloudTeamDetail.self, from: Data(datedLink.utf8)) + #expect(withExpiry.links.first?.expiresAt == Date(timeIntervalSince1970: 1_791_806_400)) + + // A string that is not a date still has to fail, so that accepting two + // shapes does not turn into accepting anything. + let garbage = detailJSON.replacingOccurrences(of: "2026-10-05T12:00:00.000Z", with: "whenever") + #expect(throws: DecodingError.self) { + try TeamsClient.decoder.decode(CloudTeamDetail.self, from: Data(garbage.utf8)) + } + } + @Test("maps the team API error envelope to a coded error") func mapsErrorEnvelope() { let body = Data(#"{"error":{"code":"seat_limit","message":"This plan includes 3 members."}}"#.utf8) diff --git a/Packages/macOS/CmuxCommandPalette/Sources/CmuxCommandPalette/Context/CommandPaletteContextKeys.swift b/Packages/macOS/CmuxCommandPalette/Sources/CmuxCommandPalette/Context/CommandPaletteContextKeys.swift index e10ee7d3886..33e6b80078c 100644 --- a/Packages/macOS/CmuxCommandPalette/Sources/CmuxCommandPalette/Context/CommandPaletteContextKeys.swift +++ b/Packages/macOS/CmuxCommandPalette/Sources/CmuxCommandPalette/Context/CommandPaletteContextKeys.swift @@ -53,6 +53,8 @@ public struct CommandPaletteContextKeys: Hashable, Sendable { public static let panelBrowserFocusModeActive = CommandPaletteContextKeys(rawValue: "panel.browserFocusModeActive") /// Whether the browser omnibar is visible. public static let panelBrowserOmnibarVisible = CommandPaletteContextKeys(rawValue: "panel.browser.omnibarVisible") + /// Whether the browser page is pinned to stay active while hidden. + public static let panelBrowserKeepsPageActive = CommandPaletteContextKeys(rawValue: "panel.browser.keepsPageActive") /// Whether the focused panel is markdown. public static let panelIsMarkdown = CommandPaletteContextKeys(rawValue: "panel.isMarkdown") /// Whether the focused panel is a native Simulator. diff --git a/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift b/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift index 314aaf8bd7d..ac09e56a9ff 100644 --- a/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift +++ b/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift @@ -1817,14 +1817,32 @@ enum CmuxEmbeddedConfigSchema { "discardHiddenWebViews": { "type": "boolean", "default": true, - "description": "Allow hidden browser tabs to release page memory and restore when shown again." + "description": "Allow hidden browser tabs to release page memory. Scroll position, supported form input, and history are restored when recoverable." + }, + "hiddenWebViewDiscardMode": { + "type": "string", + "enum": ["budget", "timer"], + "default": "budget", + "description": "How cmux picks hidden browser tabs to free. budget frees the tabs hidden longest once hidden tabs use more than hiddenWebViewMemoryBudgetMB; timer frees every tab hidden longer than hiddenWebViewDiscardDelaySeconds." + }, + "hiddenWebViewMemoryBudgetMB": { + "type": "integer", + "minimum": 256, + "maximum": 65536, + "default": 2048, + "description": "Megabytes of memory hidden browser tabs may use before cmux frees the tabs hidden longest. Applies when hiddenWebViewDiscardMode is budget." }, "hiddenWebViewDiscardDelaySeconds": { "type": "number", "minimum": 0, "maximum": 3600, "default": 300, - "description": "Seconds a browser tab must stay hidden before cmux frees its page memory." + "description": "Seconds a browser tab must stay hidden before cmux may free its page memory. In timer mode, every tab hidden this long is freed." + }, + "autoRestoreUnloadedPages": { + "type": "boolean", + "default": true, + "description": "Restore a browser page unloaded to save memory, or whose web process ended while hidden, as soon as its tab is shown. When false, the tab shows the page's last snapshot until you click Restore." }, "askWhereToSaveDownloads": { "type": "boolean", diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BrowserCatalogSection.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BrowserCatalogSection.swift index bae01b39650..9ca24b133b9 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BrowserCatalogSection.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/BrowserCatalogSection.swift @@ -51,6 +51,28 @@ public struct BrowserCatalogSection: SettingCatalogSection { userDefaultsKey: "browserHiddenWebViewDiscardDelaySeconds" ) + public let hiddenWebViewDiscardMode = DefaultsKey( + id: "browser.hiddenWebViewDiscardMode", + defaultValue: .memoryBudget, + userDefaultsKey: "browserHiddenWebViewDiscardMode" + ) + + /// Physical memory, in megabytes, that hidden browser panes may keep + /// before the pane hidden longest is discarded. + public let hiddenWebViewMemoryBudgetMB = DefaultsKey( + id: "browser.hiddenWebViewMemoryBudgetMB", + defaultValue: 2048, + userDefaultsKey: "browserHiddenWebViewMemoryBudgetMB" + ) + + /// Whether a page unloaded to save memory restores as soon as its pane + /// is shown. When off, the pane waits for the user to restore it. + public let autoRestoreUnloadedPages = DefaultsKey( + id: "browser.autoRestoreUnloadedPages", + defaultValue: true, + userDefaultsKey: "browserAutoRestoreUnloadedPages" + ) + /// Shows a save panel for each browser download instead of saving directly /// to the user's Downloads folder. public let askWhereToSaveDownloads = DefaultsKey( diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/BrowserHiddenWebViewDiscardMode.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/BrowserHiddenWebViewDiscardMode.swift new file mode 100644 index 00000000000..e36f113a625 --- /dev/null +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/BrowserHiddenWebViewDiscardMode.swift @@ -0,0 +1,10 @@ +import Foundation + +/// When hidden browser panes give their web content process back to the system. +public enum BrowserHiddenWebViewDiscardMode: String, CaseIterable, Sendable, SettingCodable { + /// Keep hidden panes alive until their web content exceeds the memory + /// budget, then discard the pane hidden longest, like a Chrome tab discard. + case memoryBudget = "budget" + /// Discard every hidden pane once it has been hidden for the delay. + case timer +} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+BrowserMemorySaver.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+BrowserMemorySaver.swift new file mode 100644 index 00000000000..207bb3503b2 --- /dev/null +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+BrowserMemorySaver.swift @@ -0,0 +1,50 @@ +import Foundation + +extension Array where Element == CuratedSettingEntry { + /// `leading`, then ``browserMemorySaverEntries``, then `trailing`. Like + /// ``appendingDevicesEntries(to:)``, a call rather than `+` keeps the + /// contextual type of ``cmuxDefault(catalog:)``'s large literals concrete. + static func insertingBrowserMemorySaverEntries( + between leading: [CuratedSettingEntry], + and trailing: [CuratedSettingEntry] + ) -> [CuratedSettingEntry] { + leading + browserMemorySaverEntries + trailing + } + + /// Search entries for the Browser Memory Saver rows in Settings > Browser + /// (`BrowserMemorySaverSettingsRows`), in row order. + static var browserMemorySaverEntries: [CuratedSettingEntry] { + [ + .init( + section: .browser, + id: "hidden-webview-discard", + title: String(localized: "settings.browser.hiddenWebViewDiscard", defaultValue: "Browser Memory Saver"), + synonyms: "Browser Memory Saver browser.discardHiddenWebViews memory hidden tabs webview discard unload reclaim" + ), + .init( + section: .browser, + id: "hidden-webview-discard-mode", + title: String(localized: "settings.browser.hiddenWebViewDiscardMode", defaultValue: "Memory Saver Mode"), + synonyms: "Memory Saver Mode browser.hiddenWebViewDiscardMode memory budget timer hidden tabs discard unload" + ), + .init( + section: .browser, + id: "hidden-webview-memory-budget", + title: String(localized: "settings.browser.hiddenWebViewMemoryBudget", defaultValue: "Hidden Tab Memory Budget"), + synonyms: "Hidden Tab Memory Budget browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes mb gb hidden tabs discard" + ), + .init( + section: .browser, + id: "hidden-webview-discard-delay", + title: String(localized: "settings.browser.hiddenWebViewDiscardDelay", defaultValue: "Memory Saver Delay"), + synonyms: "Memory Saver Delay browser.hiddenWebViewDiscardDelaySeconds memory hidden tabs delay seconds discard unload" + ), + .init( + section: .browser, + id: "unloaded-page-auto-restore", + title: String(localized: "settings.browser.autoRestoreUnloadedPages", defaultValue: "Restore Unloaded Pages Automatically"), + synonyms: "Restore Unloaded Pages Automatically browser.autoRestoreUnloadedPages reload restore unloaded discarded hidden tabs placeholder" + ) + ] + } +} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index f64c6b88f01..0fb0699e4c1 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -17,7 +17,7 @@ extension Array where Element == CuratedSettingEntry { /// a different set of entries pass their own array via /// ``SettingsSearchIndex/init(catalog:curatedEntries:)``. public static func cmuxDefault(catalog: SettingCatalog) -> [CuratedSettingEntry] { - appendingDevicesEntries(to: [ + appendingDevicesEntries(to: insertingBrowserMemorySaverEntries(between: [ // Account / integrations .init(section: .account, id: "account", title: String(localized: "settings.section.account", defaultValue: "Account"), synonyms: "Account auth authentication login logout signin sign-in signout sign-out email user profile stack team"), .init(section: .automation, id: "claude-code", title: String(localized: "settings.automation.claudeCode", defaultValue: "Claude Code Integration"), synonyms: "Claude Code Integration automation.claudeCodeIntegration claude code hooks agent integration status notifications"), @@ -517,8 +517,7 @@ extension Array where Element == CuratedSettingEntry { .init(section: .browser, id: "enable-browser", title: String(localized: "settings.browser.enabled", defaultValue: "Enable cmux Browser"), synonyms: "Enable cmux Browser browser.disabled enable disable webview embedded browser tabs links"), .init(section: .browser, id: "search-engine", title: String(localized: "settings.browser.searchEngine", defaultValue: "Default Search Engine"), synonyms: "Default Search Engine browser.defaultSearchEngine omnibar address bar google duckduckgo bing kagi brave startpage perplexity exa yahoo ecosia qwant mojeek wikipedia github baidu yandex custom search provider engine name url template"), .init(section: .browser, id: "search-suggestions", title: String(localized: "settings.browser.searchSuggestions", defaultValue: "Show Search Suggestions"), synonyms: "Show Search Suggestions browser.showSearchSuggestions suggest autocomplete address bar search suggestions"), - .init(section: .browser, id: "hidden-webview-discard", title: String(localized: "settings.browser.hiddenWebViewDiscard", defaultValue: "Browser Memory Saver"), synonyms: "Browser Memory Saver browser.discardHiddenWebViews memory hidden tabs webview discard unload reclaim"), - .init(section: .browser, id: "hidden-webview-discard-delay", title: String(localized: "settings.browser.hiddenWebViewDiscardDelay", defaultValue: "Memory Saver Delay"), synonyms: "Memory Saver Delay browser.hiddenWebViewDiscardDelaySeconds memory hidden tabs delay seconds discard unload"), + ], and: [ .init( section: .browser, id: "ask-where-to-save-downloads", @@ -584,7 +583,7 @@ extension Array where Element == CuratedSettingEntry { // Reset .init(section: .reset, id: "reset-all", title: String(localized: "settings.reset.resetAll", defaultValue: "Reset All Settings"), synonyms: "Reset All Settings factory reset restore defaults clear preferences"), - ]) + terminalGhosttyOptionEntries + ])) + terminalGhosttyOptionEntries } private static var keyboardShortcutActionSynonyms: String { diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserMemorySaverSettingsRows.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserMemorySaverSettingsRows.swift new file mode 100644 index 00000000000..b5156b42e3f --- /dev/null +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserMemorySaverSettingsRows.swift @@ -0,0 +1,134 @@ +import CmuxFoundation +import CmuxSettings +import Foundation +import SwiftUI + +/// Browser Memory Saver rows in Settings > Browser: the on/off toggle, how +/// hidden tabs are picked (memory budget or timer), the hidden tab memory +/// budget, the delay before a hidden tab may be freed, and whether a freed +/// tab restores on its own when shown. +@MainActor +struct BrowserMemorySaverSettingsRows: View { + let controlWidth: CGFloat + + @LiveSetting(\.browser.discardHiddenWebViews) private var enabled + @LiveSetting(\.browser.hiddenWebViewDiscardMode) private var mode + @LiveSetting(\.browser.hiddenWebViewMemoryBudgetMB) private var memoryBudgetMB + @LiveSetting(\.browser.hiddenWebViewDiscardDelaySeconds) private var delay + @LiveSetting(\.browser.autoRestoreUnloadedPages) private var autoRestore + + var body: some View { + SettingsCardRow( + configurationReview: .json("browser.discardHiddenWebViews"), + String(localized: "settings.browser.hiddenWebViewDiscard", defaultValue: "Browser Memory Saver"), + subtitle: String(localized: "settings.browser.hiddenWebViewDiscard.subtitle", defaultValue: "Frees page memory from hidden browser tabs. Scroll position, form input, and history come back when a tab is shown again.") + ) { + Toggle("", isOn: $enabled) + .labelsHidden() + .controlSize(.small) + .accessibilityLabel(String(localized: "settings.browser.hiddenWebViewDiscard", defaultValue: "Browser Memory Saver")) + .accessibilityIdentifier("SettingsBrowserHiddenWebViewDiscardToggle") + } + SettingsCardDivider() + + SettingsCardRow( + configurationReview: .json("browser.hiddenWebViewDiscardMode"), + String(localized: "settings.browser.hiddenWebViewDiscardMode", defaultValue: "Memory Saver Mode"), + subtitle: String(localized: "settings.browser.hiddenWebViewDiscardMode.subtitle", defaultValue: "Memory Budget frees the tabs hidden longest once hidden tabs use more than the budget. Timer frees every tab hidden longer than the delay."), + controlWidth: controlWidth + ) { + Picker("", selection: $mode) { + ForEach(BrowserHiddenWebViewDiscardMode.allCases, id: \.self) { mode in + Text(Self.displayName(mode)).tag(mode) + } + } + .labelsHidden() + .pickerStyle(.menu) + .disabled(!enabled) + .accessibilityLabel(String(localized: "settings.browser.hiddenWebViewDiscardMode", defaultValue: "Memory Saver Mode")) + .accessibilityIdentifier("SettingsBrowserHiddenWebViewDiscardModePicker") + } + SettingsCardDivider() + + SettingsCardRow( + configurationReview: .json("browser.hiddenWebViewMemoryBudgetMB"), + String(localized: "settings.browser.hiddenWebViewMemoryBudget", defaultValue: "Hidden Tab Memory Budget"), + subtitle: String(localized: "settings.browser.hiddenWebViewMemoryBudget.subtitle", defaultValue: "How much memory hidden browser tabs may use before cmux frees the ones hidden longest."), + controlWidth: controlWidth + ) { + HStack(spacing: 8) { + Text(Self.formatMemoryBudget(memoryBudgetMB)) + .cmuxFont(.body, design: .monospaced) + .monospacedDigit() + .frame(width: 72, alignment: .trailing) + Stepper("", value: $memoryBudgetMB, in: 256...65_536, step: 256) + .labelsHidden() + } + .disabled(!enabled || mode != .memoryBudget) + .accessibilityLabel(String(localized: "settings.browser.hiddenWebViewMemoryBudget", defaultValue: "Hidden Tab Memory Budget")) + .accessibilityIdentifier("SettingsBrowserHiddenWebViewMemoryBudgetStepper") + } + SettingsCardDivider() + + SettingsCardRow( + configurationReview: .json("browser.hiddenWebViewDiscardDelaySeconds"), + String(localized: "settings.browser.hiddenWebViewDiscardDelay", defaultValue: "Memory Saver Delay"), + subtitle: String(localized: "settings.browser.hiddenWebViewDiscardDelay.subtitle", defaultValue: "How long a browser tab must stay hidden before cmux may free its page memory. Active downloads, popups, developer tools, fullscreen, and loading pages are skipped."), + controlWidth: controlWidth + ) { + HStack(spacing: 8) { + Text(Self.formatDelay(delay)) + .cmuxFont(.body, design: .monospaced) + .monospacedDigit() + .frame(width: 56, alignment: .trailing) + Stepper("", value: $delay, in: 0...3_600, step: 30) + .labelsHidden() + } + .disabled(!enabled) + .accessibilityLabel(String(localized: "settings.browser.hiddenWebViewDiscardDelay", defaultValue: "Memory Saver Delay")) + .accessibilityIdentifier("SettingsBrowserHiddenWebViewDiscardDelayStepper") + } + SettingsCardDivider() + + SettingsCardRow( + configurationReview: .json("browser.autoRestoreUnloadedPages"), + String(localized: "settings.browser.autoRestoreUnloadedPages", defaultValue: "Restore Unloaded Pages Automatically"), + subtitle: String(localized: "settings.browser.autoRestoreUnloadedPages.subtitle", defaultValue: "Brings back an unloaded page as soon as its tab is shown. When off, the tab shows the page's last snapshot until you click Restore.") + ) { + Toggle("", isOn: $autoRestore) + .labelsHidden() + .controlSize(.small) + .accessibilityLabel(String(localized: "settings.browser.autoRestoreUnloadedPages", defaultValue: "Restore Unloaded Pages Automatically")) + .accessibilityIdentifier("SettingsBrowserAutoRestoreUnloadedPagesToggle") + } + } + + private static func displayName(_ mode: BrowserHiddenWebViewDiscardMode) -> String { + switch mode { + case .memoryBudget: + String(localized: "settings.browser.hiddenWebViewDiscardMode.memoryBudget", defaultValue: "Memory Budget") + case .timer: + String(localized: "settings.browser.hiddenWebViewDiscardMode.timer", defaultValue: "Timer") + } + } + + private static func formatMemoryBudget(_ megabytes: Int) -> String { + ByteCountFormatter.string(fromByteCount: Int64(megabytes) * 1024 * 1024, countStyle: .memory) + } + + /// Formats the delay as `Xm Ys` (or `Ys`) so the stepper readout reads + /// naturally for delays measured in minutes. + private static func formatDelay(_ seconds: Double) -> String { + let total = max(0, Int(seconds.rounded())) + if total < 60 { + let format = String(localized: "settings.browser.hiddenWebViewDiscardDelay.seconds", defaultValue: "%llds") + return String.localizedStringWithFormat(format, Int64(total)) + } + if total % 60 == 0 { + let format = String(localized: "settings.browser.hiddenWebViewDiscardDelay.minutes", defaultValue: "%lldm") + return String.localizedStringWithFormat(format, Int64(total / 60)) + } + let format = String(localized: "settings.browser.hiddenWebViewDiscardDelay.minutesSeconds", defaultValue: "%lldm %llds") + return String.localizedStringWithFormat(format, Int64(total / 60), Int64(total % 60)) + } +} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift index 6c92acda57e..e36acd691bb 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift @@ -6,8 +6,8 @@ import SwiftUI /// **Browser** section — mirrors the legacy in-app section /// row-for-row inside a single `SettingsCard`: Enable cmux Browser, /// Default Search Engine, conditional Custom Search Engine fields, -/// Show Search Suggestions, Browser Memory Saver + -/// Memory Saver Delay, Open Terminal Links / Intercept open, +/// Show Search Suggestions, Browser Memory Saver rows +/// (``BrowserMemorySaverSettingsRows``), Open Terminal Links / Intercept open, /// conditional Hosts editor and the External Patterns text editor, HTTP Hosts /// Allowed in Embedded Browser editor, URL Allowlist editor, Import Browser Data /// subsection, React Grab Version, Browsing History. @@ -23,8 +23,6 @@ public struct BrowserSection: View { @State private var customURL: DefaultsValueModel @State private var suggestions: DefaultsValueModel @State private var defaultZoom: DefaultsValueModel - @State private var discardEnabled: DefaultsValueModel - @State private var discardDelay: DefaultsValueModel @State private var askWhereToSaveDownloads: DefaultsValueModel @State private var openTermLinks: DefaultsValueModel @State private var interceptOpen: DefaultsValueModel @@ -73,8 +71,6 @@ public struct BrowserSection: View { _customURL = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.customSearchEngineURLTemplate)) _suggestions = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.showSearchSuggestions)) _defaultZoom = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.defaultZoomLevel)) - _discardEnabled = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.discardHiddenWebViews)) - _discardDelay = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.hiddenWebViewDiscardDelaySeconds)) _askWhereToSaveDownloads = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.askWhereToSaveDownloads)) _openTermLinks = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.openTerminalLinksInCmuxBrowser)) _interceptOpen = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.browser.interceptTerminalOpenCommandInCmuxBrowser)) @@ -105,7 +101,7 @@ public struct BrowserSection: View { Button(String(localized: "settings.browser.history.clearDialog.cancel", defaultValue: "Cancel"), role: .cancel) {} } message: { Text(String(localized: "settings.browser.history.clearDialog.message", defaultValue: "This removes visited-page suggestions from the browser omnibar.")) - }.task { startSettingsObservation([disabled, engine, customName, customURL, suggestions, defaultZoom, discardEnabled, discardDelay, askWhereToSaveDownloads, openTermLinks, interceptOpen, hosts, external, httpAllowlist, urlAllowlist, importHint, reactGrab]) } + }.task { startSettingsObservation([disabled, engine, customName, customURL, suggestions, defaultZoom, askWhereToSaveDownloads, openTermLinks, interceptOpen, hosts, external, httpAllowlist, urlAllowlist, importHint, reactGrab]) } .task { for await _ in ManagedDevicePolicy.changeSignals() { browserManagedByPolicy = ManagedDevicePolicy().isBrowserDisableLocked( @@ -231,42 +227,8 @@ public struct BrowserSection: View { } SettingsCardDivider() - // Browser Memory Saver - SettingsCardRow( - configurationReview: .json("browser.discardHiddenWebViews"), - String(localized: "settings.browser.hiddenWebViewDiscard", defaultValue: "Browser Memory Saver"), - subtitle: String(localized: "settings.browser.hiddenWebViewDiscard.subtitle", defaultValue: "Frees memory from browser tabs hidden longer than the delay. They reload when shown again.") - ) { - Toggle("", isOn: Binding(get: { discardEnabled.current }, set: { discardEnabled.set($0) })) - .labelsHidden() - .controlSize(.small) - .accessibilityIdentifier("SettingsBrowserHiddenWebViewDiscardToggle") - } - SettingsCardDivider() - - // Memory Saver Delay - SettingsCardRow( - configurationReview: .json("browser.hiddenWebViewDiscardDelaySeconds"), - String(localized: "settings.browser.hiddenWebViewDiscardDelay", defaultValue: "Memory Saver Delay"), - subtitle: String(localized: "settings.browser.hiddenWebViewDiscardDelay.subtitle", defaultValue: "How long a browser tab must stay hidden before cmux frees its page memory. Active downloads, popups, developer tools, fullscreen, and loading pages are skipped."), - controlWidth: Self.columnWidth - ) { - HStack(spacing: 8) { - Text(formatDiscardDelay(discardDelay.current)) - .cmuxFont(.body, design: .monospaced) - .monospacedDigit() - .frame(width: 56, alignment: .trailing) - Stepper( - "", - value: Binding(get: { discardDelay.current }, set: { discardDelay.set($0) }), - in: 0...3_600, - step: 30 - ) - .labelsHidden() - } - .disabled(!discardEnabled.current) - .accessibilityIdentifier("SettingsBrowserHiddenWebViewDiscardDelayStepper") - } + // Browser Memory Saver, mode, budget and delay + BrowserMemorySaverSettingsRows(controlWidth: Self.columnWidth) SettingsCardDivider() // Download Save Prompt @@ -712,7 +674,7 @@ public struct BrowserSection: View { .controlSize(.small) .disabled(true) } - .accessibilityIdentifier("SettingsBrowserImportActions") + .accessibilityElement(children: .contain).accessibilityIdentifier("SettingsBrowserImportActions") Toggle( String(localized: "settings.browser.import.hint.show", defaultValue: "Show import hint on blank browser tabs"), isOn: Binding(get: { importHintModel.current }, set: { importHintModel.set($0) }) @@ -727,7 +689,7 @@ public struct BrowserSection: View { } .padding(.horizontal, 14) .padding(.vertical, 10) - .accessibilityIdentifier("SettingsBrowserImportSection") + .accessibilityElement(children: .contain).accessibilityIdentifier("SettingsBrowserImportSection") } private func searchEngineLabel(_ engine: BrowserSearchEngine) -> String { @@ -770,27 +732,8 @@ public struct BrowserSection: View { } } - /// Formats the Memory Saver Delay value as `Xm Ys` (or `Ys`) so - /// the stepper readout reads naturally for delays measured in - /// minutes. Matches the legacy - /// `browserHiddenWebViewDiscardDelayLabel` formatter, including - /// the localized format strings. private func formatZoomPercent(_ zoom: Double) -> String { let format = String(localized: "settings.browser.defaultZoomLevel.percent", defaultValue: "%lld%%") return String.localizedStringWithFormat(format, Int64((zoom * 100).rounded())) } - - private func formatDiscardDelay(_ seconds: Double) -> String { - let total = max(0, Int(seconds.rounded())) - if total < 60 { - let format = String(localized: "settings.browser.hiddenWebViewDiscardDelay.seconds", defaultValue: "%llds") - return String.localizedStringWithFormat(format, Int64(total)) - } - if total % 60 == 0 { - let format = String(localized: "settings.browser.hiddenWebViewDiscardDelay.minutes", defaultValue: "%lldm") - return String.localizedStringWithFormat(format, Int64(total / 60)) - } - let format = String(localized: "settings.browser.hiddenWebViewDiscardDelay.minutesSeconds", defaultValue: "%lldm %llds") - return String.localizedStringWithFormat(format, Int64(total / 60), Int64(total % 60)) - } } diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index 9ac697975d5..84f91119553 100644 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -78,7 +78,10 @@ struct SettingsRowAnchorResolutionTests { "computerUse.showInMenuBar", "browser.defaultSearchEngine", "browser.discardHiddenWebViews", + "browser.hiddenWebViewDiscardMode", + "browser.hiddenWebViewMemoryBudgetMB", "browser.hiddenWebViewDiscardDelaySeconds", + "browser.autoRestoreUnloadedPages", "browser.askWhereToSaveDownloads", "browser.hostsToOpenInEmbeddedBrowser", "browser.interceptTerminalOpenCommandInCmuxBrowser", diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsSearchIndexTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsSearchIndexTests.swift index f3a5ee6cb1a..ca1ccd243eb 100644 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsSearchIndexTests.swift +++ b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsSearchIndexTests.swift @@ -159,6 +159,25 @@ struct SettingsSearchIndexTests { }) } + /// Every Browser Memory Saver row is searchable. A main merge once took + /// main's curated entries and dropped the mode, budget and auto-restore + /// rows, so search pointed only at the old toggle and delay. + @Test(arguments: [ + ("Browser Memory Saver", "setting:browser:hidden-webview-discard"), + ("Memory Saver Mode", "setting:browser:hidden-webview-discard-mode"), + ("Hidden Tab Memory Budget", "setting:browser:hidden-webview-memory-budget"), + ("Memory Saver Delay", "setting:browser:hidden-webview-discard-delay"), + ("Restore Unloaded Pages", "setting:browser:unloaded-page-auto-restore"), + ]) + func browserMemorySaverRowsAreSearchable(query: String, expectedID: String) { + let index = SettingsSearchIndex(catalog: SettingCatalog()) + let result = index.match(query) + #expect( + result.contains { $0.id == expectedID }, + "Expected settings search for '\(query)' to include \(expectedID), got \(result.map(\.id))" + ) + } + @Test(arguments: [ ("naming", "setting:automation:workspace-auto-naming"), ("nmaing", "setting:automation:workspace-auto-naming"), diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 423ed18b5e5..83669ee2051 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1091,596 +1091,6 @@ } } }, - "cli.pr.error.ambiguousWorkspace": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: could not identify the caller workspace; run it inside a cmux terminal or pass --workspace " - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: Der aufrufende Workspace konnte nicht ermittelt werden. Führen Sie den Befehl in einem cmux-Terminal aus oder übergeben Sie --workspace ." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux pr : impossible d’identifier le workspace appelant ; exécutez la commande dans un terminal cmux ou indiquez --workspace " - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: تعذر تحديد مساحة العمل المستدعية؛ شغّله داخل طرفية cmux أو مرّر --workspace " - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: no se pudo identificar el espacio de trabajo que llama; ejecútalo en un terminal de cmux o pasa --workspace " - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux pr:無法識別呼叫端工作區;請在 cmux 終端機中執行,或傳入 --workspace " - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux pr:无法识别调用方工作区;请在 cmux 终端中运行,或传入 --workspace " - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: 호출한 워크스페이스를 식별할 수 없습니다. cmux 터미널 안에서 실행하거나 --workspace 를 전달하세요" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: 呼び出し元ワークスペースを特定できません。cmux ターミナル内で実行するか --workspace を渡してください" - } - } - } - }, - "cli.pr.error.notRepository": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux pr requires a git repository in the current directory" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux pr benötigt ein Git-Repository im aktuellen Verzeichnis" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux pr nécessite un dépôt Git dans le répertoire courant" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يتطلب cmux pr مستودع Git في الدليل الحالي" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux pr requiere un repositorio Git en el directorio actual" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 需要目前目錄中的 Git 儲存庫" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 需要当前目录中的 Git 仓库" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux pr은 현재 디렉터리에 Git 저장소가 필요합니다" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux pr には現在のディレクトリに Git リポジトリが必要です" - } - } - } - }, - "cli.pr.error.invalidSelector": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux pr expects a pull-request URL or a positive pull-request number" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux pr erwartet eine Pull-Request-URL oder eine positive Pull-Request-Nummer" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux pr attend une URL de pull request ou un numéro positif" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يتوقع cmux pr رابط طلب سحب أو رقمًا موجبًا" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux pr espera una URL de pull request o un número positivo" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 需要拉取請求網址或正整數編號" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 需要拉取请求网址或正整数编号" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux pr은 풀 리퀘스트 URL 또는 양의 풀 리퀘스트 번호를 요구합니다" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux pr にはプルリクエスト URL または正の番号が必要です" - } - } - } - }, - "cli.pr.error.lookupFailed": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux pr could not resolve the pull request; check authentication and try again" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux pr konnte den Pull Request nicht auflösen; prüfen Sie die Anmeldung und versuchen Sie es erneut" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux pr n’a pas pu résoudre la pull request ; vérifiez l’authentification puis réessayez" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذر على cmux pr العثور على طلب السحب؛ تحقق من تسجيل الدخول ثم حاول مرة أخرى" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux pr no pudo resolver la pull request; comprueba la autenticación y vuelve a intentarlo" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 無法解析拉取請求;請檢查登入狀態後再試" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 无法解析拉取请求;请检查登录状态后重试" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux pr에서 풀 리퀘스트를 확인할 수 없습니다. 인증 상태를 확인한 후 다시 시도하세요" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux pr はプルリクエストを解決できませんでした。認証を確認して再試行してください" - } - } - } - }, - "cli.pr.error.lookupMalformed": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux pr received invalid pull-request information" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux pr erhielt ungültige Pull-Request-Informationen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux pr a reçu des informations de pull request invalides" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تلقى cmux pr معلومات غير صالحة عن طلب السحب" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux pr recibió información no válida de la pull request" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 收到無效的拉取請求資訊" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux pr 收到无效的拉取请求信息" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux pr에서 잘못된 풀 리퀘스트 정보를 받았습니다" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux pr は無効なプルリクエスト情報を受け取りました" - } - } - } - }, - "cli.pr.error.repositoryMismatch": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: the pull request does not belong to the detected repository" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: Der Pull Request gehört nicht zum erkannten Repository" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "cmux pr : la pull request n’appartient pas au dépôt détecté" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: لا ينتمي طلب السحب إلى المستودع المكتشف" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: la pull request no pertenece al repositorio detectado" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux pr:拉取請求不屬於偵測到的儲存庫" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux pr:拉取请求不属于检测到的仓库" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: 풀 리퀘스트가 감지된 저장소에 속하지 않습니다" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux pr: プルリクエストは検出されたリポジトリに属していません" - } - } - } - }, - "cli.pr.usage": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Usage: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nAttach or replace a pull-request link immediately. Requires a Git repository and account authentication.\nUses the repository associated with the current directory, including its configured upstream.\nTarget: explicit workspace, caller TTY, configured workspace, then a unique worktree match.\n--window restricts resolution; ambiguous targets fail without changing focus.\nThe manual link survives branch refreshes until replaced, cleared, or the session ends.\nThe existing watcher refreshes matching pull-request status. Clear removes only the manual link.\nSidebar visibility and click settings still apply.\n\nExample:\n cmux pr 123\n cmux pr clear" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Verwendung: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nVerknüpft oder ersetzt sofort einen Pull-Request-Link. Erfordert ein Git-Repository und eine authentifizierte Anmeldung.\nVerwendet das Repository des aktuellen Verzeichnisses einschließlich des konfigurierten Upstreams.\nZiel: expliziter Workspace, aufrufendes TTY, konfigurierter Workspace, dann ein eindeutiger Worktree-Treffer.\n--window begrenzt die Suche; mehrdeutige Ziele schlagen ohne Fokuswechsel fehl.\nDer manuelle Link bleibt bei Branch-Aktualisierungen bis zum Ersetzen, Löschen oder Sitzungsende erhalten.\nDer vorhandene Watcher aktualisiert den passenden Pull-Request-Status. Clear entfernt nur den manuellen Link.\nSidebar-Sichtbarkeits- und Klickeinstellungen gelten weiterhin.\n\nBeispiel:\n cmux pr 123\n cmux pr clear" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Utilisation : cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nAssocie ou remplace immédiatement un lien de pull request. Nécessite un dépôt Git et une authentification.\nUtilise le dépôt associé au répertoire courant, y compris son amont configuré.\nCible : workspace explicite, TTY appelant, workspace configuré, puis worktree unique correspondant.\n--window restreint la recherche ; une cible ambiguë échoue sans changer le focus.\nLe lien manuel survit aux actualisations de branche jusqu’à son remplacement, sa suppression ou la fin de session.\nLe suivi existant actualise le statut de la pull request correspondante. Clear supprime uniquement le lien manuel.\nLes réglages de visibilité et de clic de la barre latérale restent applicables.\n\nExemple :\n cmux pr 123\n cmux pr clear" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "الاستخدام: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nيربط رابط طلب سحب أو يستبدله فورًا. يتطلب مستودع Git وتسجيل دخول موثق.\nيستخدم المستودع المرتبط بالدليل الحالي، بما في ذلك المنبع المضبوط.\nالهدف: مساحة عمل صريحة، ثم TTY المستدعي، ثم مساحة العمل المضبوطة، ثم تطابق worktree فريد.\nيقيّد --window البحث؛ تفشل الأهداف الملتبسة دون تغيير التركيز.\nيبقى الرابط اليدوي بعد تحديثات الفرع حتى استبداله أو مسحه أو انتهاء الجلسة.\nيحدّث المراقب الحالي حالة طلب السحب المطابق. يزيل clear الرابط اليدوي فقط.\nتظل إعدادات ظهور الشريط الجانبي والنقر سارية.\n\nمثال:\n cmux pr 123\n cmux pr clear" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Uso: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nAsocia o reemplaza inmediatamente un enlace de pull request. Requiere un repositorio Git y autenticación.\nUsa el repositorio asociado al directorio actual, incluido su upstream configurado.\nDestino: espacio explícito, TTY del llamante, espacio configurado y después un worktree coincidente único.\n--window limita la búsqueda; los destinos ambiguos fallan sin cambiar el foco.\nEl enlace manual sobrevive a las actualizaciones de rama hasta reemplazarlo, borrarlo o finalizar la sesión.\nEl observador existente actualiza el estado de la pull request coincidente. Clear solo elimina el enlace manual.\nSe siguen aplicando los ajustes de visibilidad y clic de la barra lateral.\n\nEjemplo:\n cmux pr 123\n cmux pr clear" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "用法:cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\n立即附加或取代拉取請求連結。需要 Git 儲存庫與已驗證的登入狀態。\n使用目前目錄所關聯的儲存庫,包括已設定的上游。\n目標順序:明確工作區、呼叫端 TTY、已設定工作區,然後是唯一符合的 worktree。\n--window 會限制搜尋;目標不明確時會失敗且不變更焦點。\n手動連結會保留於分支重新整理後,直到被取代、清除或工作階段結束。\n現有監看器會更新相符拉取請求的狀態。clear 只移除手動連結。\n側邊欄顯示與點擊設定仍然適用。\n\n範例:\n cmux pr 123\n cmux pr clear" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "用法:cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\n立即附加或替换拉取请求链接。需要 Git 仓库和已验证的登录状态。\n使用当前目录关联的仓库,包括已配置的上游。\n目标顺序:明确工作区、调用方 TTY、已配置工作区,然后是唯一匹配的 worktree。\n--window 限制搜索;目标不明确时会失败且不改变焦点。\n手动链接在分支刷新后仍然保留,直到被替换、清除或会话结束。\n现有监视器会更新匹配拉取请求的状态。clear 只移除手动链接。\n侧边栏显示和点击设置仍然适用。\n\n示例:\n cmux pr 123\n cmux pr clear" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사용법: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\n풀 리퀘스트 링크를 즉시 연결하거나 교체합니다. Git 저장소와 인증된 로그인이 필요합니다.\n현재 디렉터리에 연결된 저장소와 설정된 업스트림을 사용합니다.\n대상 순서: 명시적 워크스페이스, 호출 TTY, 설정된 워크스페이스, 고유한 worktree 일치 항목.\n--window는 검색 범위를 제한하며, 모호한 대상은 포커스를 바꾸지 않고 실패합니다.\n수동 링크는 교체, 삭제 또는 세션 종료까지 브랜치 새로고침 후에도 유지됩니다.\n기존 감시기가 일치하는 풀 리퀘스트 상태를 갱신합니다. clear는 수동 링크만 제거합니다.\n사이드바 표시 및 클릭 설정은 계속 적용됩니다.\n\n예:\n cmux pr 123\n cmux pr clear" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "使用方法: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nプルリクエストのリンクをすぐに追加または置換します。Git リポジトリと認証済みのログインが必要です。\n現在のディレクトリに関連付けられたリポジトリと、設定済みの上流を使用します。\n対象の優先順:明示ワークスペース、呼び出し元 TTY、設定済みワークスペース、一意の worktree 一致。\n--window は検索を制限します。対象が曖昧な場合、フォーカスを変えずに失敗します。\n手動リンクはブランチ更新後も、置換・削除またはセッション終了まで保持されます。\n既存の監視処理が一致するプルリクエストの状態を更新します。clear は手動リンクだけを削除します。\nサイドバーの表示設定とクリック設定は引き続き適用されます。\n\n例:\n cmux pr 123\n cmux pr clear" - } - } - } - }, - "cli.pr.error.workspaceMissing": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Workspace not found; run cmux list-workspaces and retry with --workspace." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Workspace nicht gefunden; führen Sie cmux list-workspaces aus und wiederholen Sie den Befehl mit --workspace." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Workspace introuvable ; lancez cmux list-workspaces puis réessayez avec --workspace." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لم يتم العثور على مساحة العمل؛ شغّل cmux list-workspaces ثم أعد المحاولة باستخدام --workspace." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se encontró el espacio de trabajo; ejecuta cmux list-workspaces y reintenta con --workspace." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "找不到工作區;請執行 cmux list-workspaces,然後使用 --workspace 重試。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "找不到工作区;请运行 cmux list-workspaces,然后使用 --workspace 重试。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "워크스페이스를 찾을 수 없습니다. cmux list-workspaces를 실행하고 --workspace로 다시 시도하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ワークスペースが見つかりません。cmux list-workspaces を実行し、--workspace で再試行してください。" - } - } - } - }, - "cli.pr.error.invalidHandoff": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Invalid PR handoff; use cmux pr --help." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Ungültige PR-Zuordnung; verwenden Sie cmux pr --help." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Association de PR invalide ; consultez cmux pr --help." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "ربط PR غير صالح؛ استخدم cmux pr --help." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Asociación de PR no válida; usa cmux pr --help." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "PR 關聯無效;請使用 cmux pr --help。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "PR 关联无效;请使用 cmux pr --help。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "잘못된 PR 연결입니다. cmux pr --help를 사용하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "無効な PR 関連付けです。cmux pr --help を参照してください。" - } - } - } - }, - "cli.help.command.pr": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "pr [flags] | pr clear [flags]" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "pr [Optionen] | pr clear [Optionen]" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "pr [options] | pr clear [options]" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "pr [الخيارات] | pr clear [الخيارات]" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "pr [opciones] | pr clear [opciones]" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "pr [旗標] | pr clear [旗標]" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "pr [标志] | pr clear [标志]" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "pr [옵션] | pr clear [옵션]" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "pr [オプション] | pr clear [オプション]" - } - } - } - }, "cli.guide.usage": { "extractionState": "manual", "localizations": { @@ -236062,6 +235472,256 @@ } } }, + "cloudTree.group.cloudMachines": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Maschinen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Machines cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أجهزة سحابية" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Máquinas en la nube" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端機器" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端机器" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 머신" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドマシン" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Mašine u oblaku" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Cloud-maskiner" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Macchine cloud" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ម៉ាស៊ីនលើពពក" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Skymaskiner" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Maszyny w chmurze" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Máquinas na nuvem" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Облачные машины" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เครื่องคลาวด์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bulut makineleri" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Хмарні машини" + } + } + } + }, + "cloudTree.group.devices": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "My Devices" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Meine Geräte" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mes appareils" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أجهزتي" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mis dispositivos" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "我的裝置" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "我的设备" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "내 기기" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "マイデバイス" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Moji uređaji" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Mine enheder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I miei dispositivi" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ឧបករណ៍របស់ខ្ញុំ" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Mine enheter" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Moje urządzenia" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Meus dispositivos" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Мои устройства" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "อุปกรณ์ของฉัน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Cihazlarım" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Мої пристрої" + } + } + } + }, "machines.operation.checkpoint": { "extractionState": "manual", "localizations": { @@ -238457,6 +238117,419 @@ } } }, + "machines.listUnavailable.stale": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Machine list unavailable — showing last known" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Maschinenliste nicht verfügbar – zeige letzten bekannten Stand" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Liste des machines indisponible — affichage des dernières informations connues" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "قائمة الأجهزة غير متاحة — يتم عرض آخر حالة معروفة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Lista de máquinas no disponible — se muestra el último estado conocido" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法取得機器清單 — 顯示上次已知的狀態" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "机器列表不可用 —— 显示的是最后已知状态" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "머신 목록을 사용할 수 없음 — 마지막으로 확인된 상태 표시 중" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "マシン一覧を取得できません — 最後の状態を表示中" + } + } + } + }, + "machines.listUnavailable.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your machines are unchanged. cmux couldn’t load the list from the Cloud service and retries on its own." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Maschinen sind unverändert. cmux konnte die Liste nicht vom Cloud-Dienst laden und versucht es selbstständig erneut." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos machines ne sont pas modifiées. cmux n’a pas pu charger la liste depuis le service Cloud et va réessayer automatiquement." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم تتغير أجهزتك. تعذر على cmux تحميل القائمة من الخدمة السحابية، وسيعيد المحاولة تلقائيًا." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tus máquinas no han cambiado. cmux no pudo cargar la lista desde el servicio de la nube y volverá a intentarlo automáticamente." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "您的機器沒有任何變動。cmux 無法從雲端服務載入清單,會自動重試。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "你的机器没有任何变化。cmux 无法从云端服务加载列表,会自动重试。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "머신은 변경되지 않았습니다. cmux가 클라우드 서비스에서 목록을 불러오지 못했으며 자동으로 다시 시도합니다." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "マシンは変更されていません。cmux はクラウドサービスから一覧を読み込めませんでした。自動的に再試行します。" + } + } + } + }, + "machines.listUnavailable.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Can’t load the machine list" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Maschinenliste kann nicht geladen werden" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de charger la liste des machines" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذر تحميل قائمة الأجهزة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se puede cargar la lista de máquinas" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法載入機器清單" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法加载机器列表" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "머신 목록을 불러올 수 없습니다" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "マシン一覧を読み込めません" + } + } + } + }, + "machines.offline.stale": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Offline — showing last known" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Offline – zeige letzten bekannten Stand" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hors ligne — affichage des dernières informations connues" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "غير متصل — يتم عرض آخر حالة معروفة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin conexión — se muestra el último estado conocido" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "離線 — 顯示上次已知的狀態" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "离线 —— 显示的是最后已知状态" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오프라인 — 마지막으로 확인된 상태 표시 중" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "オフライン — 最後の状態を表示中" + } + } + } + }, + "machines.offline.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud machines load when this Mac is back online." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Maschinen werden geladen, sobald dieser Mac wieder online ist." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les machines Cloud se chargeront dès que ce Mac sera de nouveau en ligne." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "سيتم تحميل الأجهزة السحابية عندما يعود جهاز Mac هذا إلى الاتصال بالإنترنت." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las máquinas en la nube se cargarán cuando este Mac vuelva a estar en línea." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "這台 Mac 恢復連線後,就會載入雲端機器。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "这台 Mac 恢复联网后,就会加载云端机器。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 Mac이 다시 온라인 상태가 되면 클라우드 머신을 불러옵니다." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "この Mac がオンラインに戻ると Cloud マシンを読み込みます。" + } + } + } + }, + "machines.offline.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for network" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Warte auf Netzwerk" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En attente du réseau" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "في انتظار الشبكة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esperando la red" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在等待網路" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在等待网络" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "네트워크 대기 중" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ネットワークを待機中" + } + } + } + }, + "machines.reconnecting.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reconnecting to Cloud…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung zur Cloud wird wiederhergestellt…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reconnexion au Cloud…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ إعادة الاتصال بالسحابة…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reconectando con la nube…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在重新連線至雲端…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在重新连接云端…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드에 다시 연결하는 중…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドに再接続中…" + } + } + } + }, "markdown.copyConfirm.html": { "extractionState": "manual", "localizations": { @@ -314010,55 +314083,55 @@ "en": { "stringUnit": { "state": "translated", - "value": "How long a browser tab must stay hidden before cmux frees its page memory. Active downloads, popups, developer tools, fullscreen, and loading pages are skipped." + "value": "How long a browser tab must stay hidden before cmux may free its page memory. Active downloads, popups, developer tools, fullscreen, and loading pages are skipped." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ブラウザタブが非表示になってから、cmux がページメモリを解放するまでの時間です。進行中のダウンロード、ポップアップ、開発者ツール、フルスクリーン、読み込み中のページは対象外です。" + "value": "ブラウザタブが非表示になってから、cmux がページメモリを解放できるようになるまでの時間です。進行中のダウンロード、ポップアップ、開発者ツール、フルスクリーン、読み込み中のページは対象外です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "浏览器标签页需隐藏多久,cmux 才会释放其页面内存。正在下载、弹出窗口、开发者工具、全屏和加载中的页面会被跳过。" + "value": "浏览器标签页需隐藏多久,cmux 才可以释放其页面内存。正在下载、弹出窗口、开发者工具、全屏和加载中的页面会被跳过。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Wie lange ein Browser-Tab ausgeblendet bleiben muss, bevor cmux seinen Seitenspeicher freigibt. Aktive Downloads, Popups, Entwicklertools, Vollbild und ladende Seiten werden übersprungen." + "value": "Wie lange ein Browser-Tab ausgeblendet bleiben muss, bevor cmux seinen Seitenspeicher freigeben darf. Aktive Downloads, Popups, Entwicklertools, Vollbild und ladende Seiten werden übersprungen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Durée pendant laquelle un onglet de navigateur doit rester masqué avant que cmux ne libère sa mémoire de page. Les téléchargements actifs, les fenêtres surgissantes, les outils de développement, le mode plein écran et les pages en cours de chargement sont ignorés." + "value": "Durée pendant laquelle un onglet de navigateur doit rester masqué avant que cmux puisse libérer sa mémoire de page. Les téléchargements actifs, les fenêtres surgissantes, les outils de développement, le mode plein écran et les pages en cours de chargement sont ignorés." } }, "ar": { "stringUnit": { "state": "translated", - "value": "المدة التي يجب أن تظل فيها علامة تبويب المتصفح مخفية قبل أن يقوم cmux بتحرير ذاكرة صفحتها. يتم تخطي التنزيلات النشطة والنوافذ المنبثقة وأدوات المطورين وملء الشاشة والصفحات التي يتم تحميلها." + "value": "المدة التي يجب أن تظل فيها علامة تبويب المتصفح مخفية قبل أن يتمكن cmux من تحرير ذاكرة صفحتها. يتم تخطي التنزيلات النشطة والنوافذ المنبثقة وأدوات المطورين وملء الشاشة والصفحات التي يتم تحميلها." } }, "es": { "stringUnit": { "state": "translated", - "value": "Cuánto tiempo debe permanecer oculta una pestaña del navegador antes de que cmux libere la memoria de su página. Se omiten las descargas activas, las ventanas emergentes, las herramientas de desarrollo, la pantalla completa y las páginas en carga." + "value": "Cuánto tiempo debe permanecer oculta una pestaña del navegador antes de que cmux pueda liberar la memoria de su página. Se omiten las descargas activas, las ventanas emergentes, las herramientas de desarrollo, la pantalla completa y las páginas en carga." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "瀏覽器標籤頁必須保持隱藏多久,cmux 才會釋放其網頁記憶體。作用中的下載、彈出式視窗、開發者工具、全螢幕和載入中的網頁將會跳過。" + "value": "瀏覽器標籤頁必須保持隱藏多久,cmux 才可以釋放其網頁記憶體。作用中的下載、彈出式視窗、開發者工具、全螢幕和載入中的網頁將會跳過。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux가 페이지 메모리를 해제하기 전까지 브라우저 탭이 숨겨진 상태로 유지되어야 하는 시간입니다. 진행 중인 다운로드, 팝업, 개발자 도구, 전체 화면 및 로딩 중인 페이지는 제외됩니다." + "value": "cmux가 페이지 메모리를 해제할 수 있기 전까지 브라우저 탭이 숨겨진 상태로 유지되어야 하는 시간입니다. 진행 중인 다운로드, 팝업, 개발자 도구, 전체 화면 및 로딩 중인 페이지는 제외됩니다." } } } @@ -353705,6 +353778,124 @@ } } }, + "settings.search.alias.setting.browser.hidden-webview-discard-mode": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload メモリセーバー モード 予算 タイマー 非表示 タブ 破棄 解放" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload Speichersparmodus Modus Budget zeitgesteuert ausgeblendete Tabs verwerfen entladen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload économiseur de mémoire mode budget minuteur onglets masqués rejeter décharger" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload توفير الذاكرة وضع ميزانية مؤقت علامات تبويب مخفية تجاهل إلغاء تحميل" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload ahorro de memoria modo presupuesto temporizador pestañas ocultas descartar descargar" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload 記憶體節省 方式 預算 計時器 隱藏 標籤頁 捨棄 卸載" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload 内存节省 方式 预算 计时器 隐藏 标签页 丢弃 卸载" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewDiscardMode memory saver budget timer hidden tabs discard unload 메모리 절약 방식 예산 타이머 숨겨진 탭 삭제 언로드" + } + } + } + }, + "settings.search.alias.setting.browser.hidden-webview-memory-budget": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload メモリ 予算 上限 メガバイト 非表示 タブ 破棄 解放" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload Speicher Budget Limit Megabyte ausgeblendete Tabs verwerfen entladen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload mémoire budget limite mégaoctets onglets masqués rejeter décharger" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload ذاكرة ميزانية حد ميغابايت علامات تبويب مخفية تجاهل إلغاء تحميل" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload memoria presupuesto límite megabytes pestañas ocultas descartar descargar" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload 記憶體 預算 上限 MB 隱藏 標籤頁 捨棄 卸載" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload 内存 预算 上限 MB 隐藏 标签页 丢弃 卸载" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "browser.hiddenWebViewMemoryBudgetMB memory budget limit megabytes hidden tabs discard unload 메모리 예산 한도 메가바이트 숨겨진 탭 삭제 언로드" + } + } + } + }, "settings.search.alias.setting.browser.hidden-webview-discard-delay": { "extractionState": "manual", "localizations": { @@ -556376,55 +556567,55 @@ "en": { "stringUnit": { "state": "translated", - "value": "Frees memory from browser tabs hidden longer than the delay. They reload when shown again." + "value": "Frees page memory from hidden browser tabs. Scroll position, form input, and history come back when a tab is shown again." } }, "de": { "stringUnit": { "state": "translated", - "value": "Browser-Tabs, die länger als die festgelegte Zeit ausgeblendet sind, geben ihren Speicher frei und werden beim erneuten Anzeigen neu geladen." + "value": "Gibt den Seitenspeicher ausgeblendeter Browser-Tabs frei. Scrollposition, Formulareingaben und Verlauf werden wiederhergestellt, wenn ein Tab wieder angezeigt wird." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Libère la mémoire des onglets de navigateur masqués depuis plus longtemps que le délai défini. Ils se rechargent lorsqu’ils réapparaissent." + "value": "Libère la mémoire des pages des onglets de navigateur masqués. La position de défilement, les saisies de formulaire et l’historique sont restaurés lorsqu’un onglet réapparaît." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يحرر ذاكرة علامات تبويب المتصفح المخفية لمدة تتجاوز المهلة المحددة، ويُعاد تحميلها عند عرضها مجددًا." + "value": "يحرر ذاكرة الصفحات من علامات تبويب المتصفح المخفية. تتم استعادة موضع التمرير وإدخالات النماذج والسجل عند عرض علامة التبويب مجددًا." } }, "es": { "stringUnit": { "state": "translated", - "value": "Libera la memoria de las pestañas del navegador que llevan ocultas más tiempo del indicado. Se vuelven a cargar al mostrarse de nuevo." + "value": "Libera la memoria de página de las pestañas ocultas del navegador. La posición de desplazamiento, los datos de formularios y el historial se recuperan cuando la pestaña vuelve a mostrarse." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "瀏覽器標籤頁隱藏超過延遲時間後,會釋放其記憶體。再次顯示時會重新載入。" + "value": "釋放隱藏瀏覽器標籤頁的網頁記憶體。標籤頁再次顯示時,會恢復捲動位置、表單輸入和瀏覽記錄。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "浏览器标签页隐藏超过延迟时间后,会释放其内存。再次显示时会重新加载。" + "value": "释放隐藏浏览器标签页的页面内存。标签页再次显示时,会恢复滚动位置、表单输入和浏览历史。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "브라우저 탭이 지연 시간보다 오래 숨겨져 있으면 메모리를 해제합니다. 탭을 다시 표시하면 새로 고칩니다." + "value": "숨겨진 브라우저 탭의 페이지 메모리를 해제합니다. 탭을 다시 표시하면 스크롤 위치, 양식 입력 및 방문 기록이 복원됩니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "非表示の時間が遅延時間を超えたブラウザタブのメモリを解放します。タブは再び表示したときに再読み込みされます。" + "value": "非表示のブラウザタブのページメモリを解放します。タブを再び表示すると、スクロール位置、フォーム入力、履歴が復元されます。" } } } @@ -564931,665 +565122,828 @@ } } }, - "cloudTree.group.cloudMachines": { + "browser.discard.restoring": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud Machines" + "value": "Restoring…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ الاستعادة…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen" + "value": "Wird wiederhergestellt…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restaurando…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Machines cloud" + "value": "Restauration…" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "أجهزة سحابية" + "value": "復元中…" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Máquinas en la nube" + "value": "복원 중…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在恢复…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "雲端機器" + "value": "正在還原…" + } + } + } + }, + "settings.browser.hiddenWebViewDiscardMode": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Memory Saver Mode" } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "云端机器" + "value": "وضع توفير الذاكرة" } }, - "ko": { + "de": { "stringUnit": { "state": "translated", - "value": "클라우드 머신" + "value": "Speichersparmodus-Art" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Modo de ahorro de memoria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode de l’économiseur de mémoire" } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウドマシン" + "value": "メモリセーバーのモード" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Mašine u oblaku" + "value": "메모리 절약 방식" } }, - "da": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud-maskiner" + "value": "内存节省方式" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Macchine cloud" + "value": "記憶體節省方式" + } + } + } + }, + "settings.browser.hiddenWebViewMemoryBudget": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hidden Tab Memory Budget" } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "ម៉ាស៊ីនលើពពក" + "value": "ميزانية ذاكرة علامات التبويب المخفية" } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Skymaskiner" + "value": "Speicherbudget für ausgeblendete Tabs" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Maszyny w chmurze" + "value": "Presupuesto de memoria de pestañas ocultas" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Máquinas na nuvem" + "value": "Budget mémoire des onglets masqués" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Облачные машины" + "value": "非表示タブのメモリ予算" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "เครื่องคลาวด์" + "value": "숨겨진 탭 메모리 예산" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Bulut makineleri" + "value": "隐藏标签页内存预算" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Хмарні машини" + "value": "隱藏標籤頁記憶體預算" } } } }, - "cloudTree.group.devices": { + "settings.browser.hiddenWebViewDiscardMode.memoryBudget": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "My Devices" + "value": "Memory Budget" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ميزانية الذاكرة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Meine Geräte" + "value": "Speicherbudget" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Presupuesto de memoria" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Mes appareils" + "value": "Budget mémoire" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "メモリ予算" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "메모리 예산" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "内存预算" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "記憶體預算" + } + } + } + }, + "settings.browser.hiddenWebViewDiscardMode.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Memory Budget frees the tabs hidden longest once hidden tabs use more than the budget. Timer frees every tab hidden longer than the delay." } }, "ar": { "stringUnit": { "state": "translated", - "value": "أجهزتي" + "value": "يحرر «ميزانية الذاكرة» علامات التبويب المخفية منذ أطول مدة عندما تتجاوز علامات التبويب المخفية الميزانية. ويحرر «المؤقت» كل علامة تبويب مخفية لمدة أطول من التأخير." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Speicherbudget“ gibt die am längsten ausgeblendeten Tabs frei, sobald ausgeblendete Tabs mehr als das Budget belegen. „Zeitgesteuert“ gibt jeden Tab frei, der länger als die Verzögerung ausgeblendet ist." } }, "es": { "stringUnit": { "state": "translated", - "value": "Mis dispositivos" + "value": "«Presupuesto de memoria» libera las pestañas ocultas desde hace más tiempo cuando las pestañas ocultas superan el presupuesto. «Temporizador» libera cada pestaña oculta durante más tiempo que la demora." } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "我的裝置" + "value": "« Budget mémoire » libère les onglets masqués depuis le plus longtemps dès que les onglets masqués dépassent le budget. « Minuteur » libère chaque onglet masqué depuis plus longtemps que le délai." } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "我的设备" + "value": "「メモリ予算」は、非表示のタブが予算を超えてメモリを使うと、最も長く非表示になっているタブから解放します。「タイマー」は、遅延時間より長く非表示になっているすべてのタブを解放します。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "내 기기" + "value": "'메모리 예산'은 숨겨진 탭이 예산보다 많은 메모리를 사용하면 가장 오래 숨겨진 탭부터 해제합니다. '타이머'는 지연 시간보다 오래 숨겨진 모든 탭을 해제합니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "“内存预算”会在隐藏标签页占用的内存超过预算时,释放隐藏最久的标签页。“计时器”会释放隐藏时间超过延迟的每个标签页。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "「記憶體預算」會在隱藏標籤頁使用的記憶體超過預算時,釋放隱藏最久的標籤頁。「計時器」會釋放隱藏時間超過延遲的每個標籤頁。" + } + } + } + }, + "settings.browser.hiddenWebViewDiscardMode.timer": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Timer" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "المؤقت" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeitgesteuert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Temporizador" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Minuteur" } }, "ja": { "stringUnit": { "state": "translated", - "value": "マイデバイス" + "value": "タイマー" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Moji uređaji" + "value": "타이머" } }, - "da": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Mine enheder" + "value": "计时器" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "I miei dispositivi" + "value": "計時器" + } + } + } + }, + "settings.browser.hiddenWebViewMemoryBudget.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How much memory hidden browser tabs may use before cmux frees the ones hidden longest." } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "ឧបករណ៍របស់ខ្ញុំ" + "value": "مقدار الذاكرة التي يمكن أن تستخدمها علامات تبويب المتصفح المخفية قبل أن يحرر cmux العلامات المخفية منذ أطول مدة." } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Mine enheter" + "value": "Wie viel Speicher ausgeblendete Browser-Tabs belegen dürfen, bevor cmux die am längsten ausgeblendeten freigibt." } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Moje urządzenia" + "value": "Cuánta memoria pueden usar las pestañas ocultas del navegador antes de que cmux libere las que llevan más tiempo ocultas." } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Meus dispositivos" + "value": "Quantité de mémoire que les onglets de navigateur masqués peuvent utiliser avant que cmux libère ceux masqués depuis le plus longtemps." } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Мои устройства" + "value": "cmux が最も長く非表示になっているタブを解放するまでに、非表示のブラウザタブが使用できるメモリ量です。" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "อุปกรณ์ของฉัน" + "value": "cmux가 가장 오래 숨겨진 탭을 해제하기 전까지 숨겨진 브라우저 탭이 사용할 수 있는 메모리 양입니다." } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cihazlarım" + "value": "隐藏的浏览器标签页可使用的内存上限,超过后 cmux 会释放隐藏最久的标签页。" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Мої пристрої" + "value": "隱藏的瀏覽器標籤頁可使用的記憶體上限,超過後 cmux 會釋放隱藏最久的標籤頁。" } } } }, - "machines.listUnavailable.stale": { + "command.browserKeepPageActive.enable.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Machine list unavailable — showing last known" + "value": "Keep Page Active While Hidden" } }, "de": { "stringUnit": { "state": "translated", - "value": "Maschinenliste nicht verfügbar – zeige letzten bekannten Stand" + "value": "Seite im Hintergrund aktiv halten" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Liste des machines indisponible — affichage des dernières informations connues" + "value": "Garder la page active lorsqu’elle est masquée" } }, "ar": { "stringUnit": { "state": "translated", - "value": "قائمة الأجهزة غير متاحة — يتم عرض آخر حالة معروفة" + "value": "إبقاء الصفحة نشطة أثناء إخفائها" } }, "es": { "stringUnit": { "state": "translated", - "value": "Lista de máquinas no disponible — se muestra el último estado conocido" + "value": "Mantener la página activa mientras está oculta" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法取得機器清單 — 顯示上次已知的狀態" + "value": "隱藏時保持頁面啟用" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "机器列表不可用 —— 显示的是最后已知状态" + "value": "隐藏时保持页面活跃" } }, "ko": { "stringUnit": { "state": "translated", - "value": "머신 목록을 사용할 수 없음 — 마지막으로 확인된 상태 표시 중" + "value": "숨겨져 있을 때 페이지 활성 상태 유지" } }, "ja": { "stringUnit": { "state": "translated", - "value": "マシン一覧を取得できません — 最後の状態を表示中" + "value": "非表示中もページをアクティブに保つ" } } } }, - "machines.listUnavailable.subtitle": { + "command.browserKeepPageActive.disable.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Your machines are unchanged. cmux couldn’t load the list from the Cloud service and retries on its own." + "value": "Allow Page to Unload While Hidden" } }, "de": { "stringUnit": { "state": "translated", - "value": "Deine Maschinen sind unverändert. cmux konnte die Liste nicht vom Cloud-Dienst laden und versucht es selbstständig erneut." + "value": "Entladen der Seite im Hintergrund erlauben" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Vos machines ne sont pas modifiées. cmux n’a pas pu charger la liste depuis le service Cloud et va réessayer automatiquement." + "value": "Autoriser le déchargement de la page lorsqu’elle est masquée" } }, "ar": { "stringUnit": { "state": "translated", - "value": "لم تتغير أجهزتك. تعذر على cmux تحميل القائمة من الخدمة السحابية، وسيعيد المحاولة تلقائيًا." + "value": "السماح بإلغاء تحميل الصفحة أثناء إخفائها" } }, "es": { "stringUnit": { "state": "translated", - "value": "Tus máquinas no han cambiado. cmux no pudo cargar la lista desde el servicio de la nube y volverá a intentarlo automáticamente." + "value": "Permitir descargar la página mientras está oculta" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "您的機器沒有任何變動。cmux 無法從雲端服務載入清單,會自動重試。" + "value": "允許在隱藏時卸載頁面" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "你的机器没有任何变化。cmux 无法从云端服务加载列表,会自动重试。" + "value": "允许在隐藏时卸载页面" } }, "ko": { "stringUnit": { "state": "translated", - "value": "머신은 변경되지 않았습니다. cmux가 클라우드 서비스에서 목록을 불러오지 못했으며 자동으로 다시 시도합니다." + "value": "숨겨져 있을 때 페이지 언로드 허용" } }, "ja": { "stringUnit": { "state": "translated", - "value": "マシンは変更されていません。cmux はクラウドサービスから一覧を読み込めませんでした。自動的に再試行します。" + "value": "非表示中のページのアンロードを許可" } } } }, - "machines.listUnavailable.title": { + "browser.discard.unloadedMessage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Can’t load the machine list" + "value": "This page was unloaded to save memory." } }, "de": { "stringUnit": { "state": "translated", - "value": "Maschinenliste kann nicht geladen werden" + "value": "Diese Seite wurde entladen, um Arbeitsspeicher zu sparen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Impossible de charger la liste des machines" + "value": "Cette page a été déchargée pour économiser de la mémoire." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تعذر تحميل قائمة الأجهزة" + "value": "تم إلغاء تحميل هذه الصفحة لتوفير الذاكرة." } }, "es": { "stringUnit": { "state": "translated", - "value": "No se puede cargar la lista de máquinas" + "value": "Esta página se descargó para ahorrar memoria." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法載入機器清單" + "value": "已卸載此網頁以節省記憶體。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无法加载机器列表" + "value": "已卸载此页面以节省内存。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "머신 목록을 불러올 수 없습니다" + "value": "메모리를 절약하기 위해 이 페이지를 언로드했습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "マシン一覧を読み込めません" + "value": "メモリを節約するため、このページはアンロードされました。" } } } }, - "machines.offline.stale": { + "browser.discard.restore": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Offline — showing last known" + "value": "Restore" } }, "de": { "stringUnit": { "state": "translated", - "value": "Offline – zeige letzten bekannten Stand" + "value": "Wiederherstellen" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Hors ligne — affichage des dernières informations connues" + "value": "Restaurer" } }, "ar": { "stringUnit": { "state": "translated", - "value": "غير متصل — يتم عرض آخر حالة معروفة" + "value": "استعادة" } }, "es": { "stringUnit": { "state": "translated", - "value": "Sin conexión — se muestra el último estado conocido" + "value": "Restaurar" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "離線 — 顯示上次已知的狀態" + "value": "還原" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "离线 —— 显示的是最后已知状态" + "value": "恢复" } }, "ko": { "stringUnit": { "state": "translated", - "value": "오프라인 — 마지막으로 확인된 상태 표시 중" + "value": "복원" } }, "ja": { "stringUnit": { "state": "translated", - "value": "オフライン — 最後の状態を表示中" + "value": "復元" } } } }, - "machines.offline.subtitle": { + "settings.browser.autoRestoreUnloadedPages": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud machines load when this Mac is back online." + "value": "Restore Unloaded Pages Automatically" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen werden geladen, sobald dieser Mac wieder online ist." + "value": "Entladene Seiten automatisch wiederherstellen" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les machines Cloud se chargeront dès que ce Mac sera de nouveau en ligne." + "value": "Restaurer automatiquement les pages déchargées" } }, "ar": { "stringUnit": { "state": "translated", - "value": "سيتم تحميل الأجهزة السحابية عندما يعود جهاز Mac هذا إلى الاتصال بالإنترنت." + "value": "استعادة الصفحات التي أُلغي تحميلها تلقائيًا" } }, "es": { "stringUnit": { "state": "translated", - "value": "Las máquinas en la nube se cargarán cuando este Mac vuelva a estar en línea." + "value": "Restaurar automáticamente las páginas descargadas" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "這台 Mac 恢復連線後,就會載入雲端機器。" + "value": "自動還原已卸載的網頁" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "这台 Mac 恢复联网后,就会加载云端机器。" + "value": "自动恢复已卸载的页面" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 Mac이 다시 온라인 상태가 되면 클라우드 머신을 불러옵니다." + "value": "언로드된 페이지 자동 복원" } }, "ja": { "stringUnit": { "state": "translated", - "value": "この Mac がオンラインに戻ると Cloud マシンを読み込みます。" + "value": "アンロードされたページを自動的に復元" } } } }, - "machines.offline.title": { + "settings.browser.autoRestoreUnloadedPages.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Waiting for network" + "value": "Brings back an unloaded page as soon as its tab is shown. When off, the tab shows the page's last snapshot until you click Restore." } }, "de": { "stringUnit": { "state": "translated", - "value": "Warte auf Netzwerk" + "value": "Stellt eine entladene Seite wieder her, sobald ihr Tab angezeigt wird. Wenn deaktiviert, zeigt der Tab die letzte Momentaufnahme der Seite, bis Sie auf „Wiederherstellen“ klicken." } }, "fr": { "stringUnit": { "state": "translated", - "value": "En attente du réseau" + "value": "Restaure une page déchargée dès que son onglet est affiché. Si cette option est désactivée, l’onglet affiche le dernier aperçu de la page jusqu’à ce que vous cliquiez sur Restaurer." } }, "ar": { "stringUnit": { "state": "translated", - "value": "في انتظار الشبكة" + "value": "يستعيد الصفحة التي أُلغي تحميلها بمجرد عرض علامة تبويبها. عند إيقاف هذا الخيار، تعرض علامة التبويب آخر لقطة للصفحة إلى أن تنقر على استعادة." } }, "es": { "stringUnit": { "state": "translated", - "value": "Esperando la red" + "value": "Recupera una página descargada en cuanto se muestra su pestaña. Si está desactivado, la pestaña muestra la última captura de la página hasta que hagas clic en Restaurar." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "正在等待網路" + "value": "標籤頁一顯示就還原已卸載的網頁。關閉時,標籤頁會顯示網頁的最後快照,直到你按一下「還原」。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "正在等待网络" + "value": "标签页一显示就恢复已卸载的页面。关闭时,标签页会显示页面的最后快照,直到你点击“恢复”。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "네트워크 대기 중" + "value": "탭이 표시되는 즉시 언로드된 페이지를 복원합니다. 끄면 복원을 클릭할 때까지 탭에 페이지의 마지막 스냅샷이 표시됩니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ネットワークを待機中" + "value": "タブが表示されるとすぐに、アンロードされたページを復元します。オフの場合、「復元」をクリックするまでタブにはページの最後のスナップショットが表示されます。" } } } }, - "machines.reconnecting.title": { + "settings.search.alias.setting.browser.unloaded-page-auto-restore": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Reconnecting to Cloud…" + "value": "browser.autoRestoreUnloadedPages restore reload unloaded discarded hidden tabs placeholder" } }, "de": { "stringUnit": { "state": "translated", - "value": "Verbindung zur Cloud wird wiederhergestellt…" + "value": "browser.autoRestoreUnloadedPages wiederherstellen neu laden entladen verworfen ausgeblendete Tabs Platzhalter" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Reconnexion au Cloud…" + "value": "browser.autoRestoreUnloadedPages restaurer recharger déchargée supprimée onglets masqués espace réservé" } }, "ar": { "stringUnit": { "state": "translated", - "value": "جارٍ إعادة الاتصال بالسحابة…" + "value": "browser.autoRestoreUnloadedPages استعادة إعادة تحميل إلغاء التحميل مهملة علامات التبويب المخفية عنصر نائب" } }, "es": { "stringUnit": { "state": "translated", - "value": "Reconectando con la nube…" + "value": "browser.autoRestoreUnloadedPages restaurar recargar descargada descartada pestañas ocultas marcador de posición" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "正在重新連線至雲端…" + "value": "browser.autoRestoreUnloadedPages 還原 重新載入 已卸載 已捨棄 隱藏標籤頁 預留位置" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "正在重新连接云端…" + "value": "browser.autoRestoreUnloadedPages 恢复 重新加载 已卸载 已丢弃 隐藏标签页 占位符" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드에 다시 연결하는 중…" + "value": "browser.autoRestoreUnloadedPages 복원 새로고침 언로드 삭제됨 숨겨진 탭 자리 표시자" } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウドに再接続中…" + "value": "browser.autoRestoreUnloadedPages 復元 再読み込み アンロード 破棄 非表示のタブ プレースホルダ" } } } @@ -567364,6 +567718,131 @@ } } }, + "devices.link.error.notDiscoverable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This Mac isn’t discoverable. On that Mac, turn on “Make this Mac discoverable” in Settings › Devices." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جهاز Mac هذا غير قابل للاكتشاف. على ذلك الجهاز، فعّل «جعل جهاز Mac هذا قابلاً للاكتشاف» في الإعدادات › الأجهزة." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Mac ist nicht auffindbar. Aktiviere auf diesem Mac unter „Einstellungen › Geräte“ die Option „Diesen Mac auffindbar machen“." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este Mac no es detectable. En ese Mac, activa «Hacer que este Mac sea detectable» en Ajustes › Dispositivos." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce Mac n’est pas détectable. Sur ce Mac, activez « Rendre ce Mac détectable » dans Réglages › Appareils." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このMacは検出可能になっていません。そのMacの「設定 › デバイス」で「このMacを検出可能にする」をオンにしてください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 Mac을 검색할 수 없습니다. 해당 Mac의 설정 › 기기에서 “이 Mac을 검색할 수 있도록 설정”을 켜세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "这台 Mac 无法被发现。请在该 Mac 的“设置 › 设备”中开启“允许发现此 Mac”。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "這部 Mac 無法被探索。請在該 Mac 的「設定 › 裝置」中開啟「允許探索此 Mac」。" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ovaj Mac nije vidljiv. Na tom Macu uključite „Učini ovaj Mac vidljivim” u Postavke › Uređaji." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Denne Mac kan ikke findes. Slå “Gør denne Mac synlig” til på den Mac under Indstillinger › Enheder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo Mac non è individuabile. Su quel Mac, attiva “Rendi individuabile questo Mac” in Impostazioni › Dispositivi." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Mac នេះមិនអាចរកឃើញបានទេ។ នៅលើ Mac នោះ សូមបើក “ធ្វើឲ្យ Mac នេះអាចរកឃើញ” ក្នុងការកំណត់ › ឧបករណ៍។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Denne Macen er ikke synlig. Slå på «Gjør denne Macen synlig» på den Macen under Innstillinger › Enheter." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ten Mac nie jest wykrywalny. Na tym Macu włącz „Uczyń ten Mac wykrywalnym” w Ustawienia › Urządzenia." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este Mac não está detectável. Nesse Mac, ative “Tornar este Mac detectável” em Ajustes › Dispositivos." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Этот Mac недоступен для обнаружения. На том Mac включите «Сделать этот Mac доступным для обнаружения» в разделе Настройки › Устройства." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "Mac เครื่องนี้ไม่สามารถค้นพบได้ บน Mac เครื่องนั้น ให้เปิด “ทำให้ Mac เครื่องนี้ค้นพบได้” ในการตั้งค่า › อุปกรณ์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu Mac keşfedilebilir değil. O Mac’te Ayarlar › Cihazlar bölümünden “Bu Mac’i keşfedilebilir yap” seçeneğini açın." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Цей Mac недоступний для виявлення. На тому Mac увімкніть «Зробити цей Mac доступним для виявлення» у розділі Налаштування › Пристрої." + } + } + } + }, "settings.automation.pi": { "extractionState": "manual", "localizations": { @@ -568114,127 +568593,615 @@ } } }, - "devices.link.error.notDiscoverable": { + "cli.auth.status.teamNotLoaded": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This Mac isn’t discoverable. On that Mac, turn on “Make this Mac discoverable” in Settings › Devices." + "value": " team: not loaded" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": " チーム: 未読み込み" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": " 团队: 未加载" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Team: nicht geladen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " équipe: non chargée" } }, "ar": { "stringUnit": { "state": "translated", - "value": "جهاز Mac هذا غير قابل للاكتشاف. على ذلك الجهاز، فعّل «جعل جهاز Mac هذا قابلاً للاكتشاف» في الإعدادات › الأجهزة." + "value": " الفريق: لم يتم تحميله" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " equipo: no cargado" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": " 團隊: 未載入" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": " 팀: 로드되지 않음" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": " tim: nije učitan" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": " team: ikke indlæst" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " team: non caricato" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": " ក្រុម៖ មិនទាន់បានផ្ទុក" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": " team: ikke lastet" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": " zespół: nie załadowano" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " equipe: não carregada" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": " команда: не загружена" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": " ทีม: ยังไม่ได้โหลด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": " ekip: yüklenmedi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": " команда: не завантажена" + } + } + } + }, + "cli.auth.status.teamNotLoadedRetrying": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " team: not loaded (retrying)" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": " チーム: 未読み込み(再試行中)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": " 团队: 未加载(正在重试)" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Mac ist nicht auffindbar. Aktiviere auf diesem Mac unter „Einstellungen › Geräte“ die Option „Diesen Mac auffindbar machen“." + "value": " Team: nicht geladen (wird erneut versucht)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " équipe: non chargée (nouvel essai)" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": " الفريق: لم يتم تحميله (جارٍ إعادة المحاولة)" } }, "es": { "stringUnit": { "state": "translated", - "value": "Este Mac no es detectable. En ese Mac, activa «Hacer que este Mac sea detectable» en Ajustes › Dispositivos." + "value": " equipo: no cargado (reintentando)" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": " 團隊: 未載入(重試中)" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": " 팀: 로드되지 않음 (재시도 중)" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": " tim: nije učitan (ponovni pokušaj)" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": " team: ikke indlæst (prøver igen)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " team: non caricato (nuovo tentativo)" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": " ក្រុម៖ មិនទាន់បានផ្ទុក (កំពុងព្យាយាមម្ដងទៀត)" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": " team: ikke lastet (prøver igjen)" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": " zespół: nie załadowano (ponawianie próby)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " equipe: não carregada (tentando novamente)" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": " команда: не загружена (повторная попытка)" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": " ทีม: ยังไม่ได้โหลด (กำลังลองอีกครั้ง)" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": " ekip: yüklenmedi (yeniden deneniyor)" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": " команда: не завантажена (повторна спроба)" + } + } + } + }, + "mobile.pairing.error.teamUnavailable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not load your cmux team. Check your internet connection, then try again." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux チームを読み込めませんでした。インターネット接続を確認してから、もう一度お試しください。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法加载你的 cmux 团队。请检查网络连接,然后重试。" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein cmux-Team konnte nicht geladen werden. Prüfe deine Internetverbindung und versuche es erneut." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de charger votre équipe cmux. Vérifiez votre connexion Internet, puis réessayez." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّر تحميل فريق cmux الخاص بك. تحقّق من اتصالك بالإنترنت، ثم حاول مرة أخرى." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo cargar tu equipo de cmux. Comprueba tu conexión a internet e inténtalo de nuevo." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法載入你的 cmux 團隊。請檢查網路連線,然後再試一次。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux 팀을 불러올 수 없습니다. 인터넷 연결을 확인한 후 다시 시도하세요." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Nije moguće učitati vaš cmux tim. Provjerite internetsku vezu i pokušajte ponovo." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Din cmux-team kunne ikke indlæses. Kontrollér din internetforbindelse, og prøv igen." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile caricare il tuo team cmux. Controlla la connessione Internet e riprova." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "មិនអាចផ្ទុកក្រុម cmux របស់អ្នកបានទេ។ សូមពិនិត្យការតភ្ជាប់អ៊ីនធឺណិត ហើយព្យាយាមម្ដងទៀត។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kunne ikke laste inn cmux-teamet ditt. Kontroller internettilkoblingen, og prøv igjen." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nie udało się załadować Twojego zespołu cmux. Sprawdź połączenie z internetem i spróbuj ponownie." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível carregar sua equipe cmux. Verifique sua conexão com a internet e tente novamente." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Не удалось загрузить вашу команду cmux. Проверьте подключение к интернету и попробуйте ещё раз." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่สามารถโหลดทีม cmux ของคุณได้ ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองอีกครั้ง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux ekibiniz yüklenemedi. İnternet bağlantınızı kontrol edip tekrar deneyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Не вдалося завантажити вашу команду cmux. Перевірте підключення до інтернету й спробуйте ще раз." + } + } + } +}, + "command.newPaneAutoLayout.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لوحة جديدة (تخطيط تلقائي)" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Novi panel (automatski raspored)" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Nyt panel (automatisk layout)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuer Bereich (automatisches Layout)" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "New Pane (Auto Layout)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nuevo panel (diseño automático)" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ce Mac n’est pas détectable. Sur ce Mac, activez « Rendre ce Mac détectable » dans Réglages › Appareils." + "value": "Nouveau panneau (disposition automatique)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovo pannello (layout automatico)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "このMacは検出可能になっていません。そのMacの「設定 › デバイス」で「このMacを検出可能にする」をオンにしてください。" + "value": "新しいペイン(自動レイアウト)" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 Mac을 검색할 수 없습니다. 해당 Mac의 설정 › 기기에서 “이 Mac을 검색할 수 있도록 설정”을 켜세요." + "value": "새 패널 (자동 레이아웃)" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Nytt panel (automatisk oppsett)" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Nowy panel (układ automatyczny)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novo Painel (Layout Automático)" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Новая панель (автоматическая раскладка)" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni Bölme (Otomatik Düzen)" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Нова панель (автоматичне розташування)" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "这台 Mac 无法被发现。请在该 Mac 的“设置 › 设备”中开启“允许发现此 Mac”。" + "value": "新建窗格(自动布局)" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "這部 Mac 無法被探索。請在該 Mac 的「設定 › 裝置」中開啟「允許探索此 Mac」。" + "value": "新增窗格(自動佈局)" + } + } + } + }, + "shortcut.newPaneAutoLayout.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لوحة جديدة (تخطيط تلقائي)" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Ovaj Mac nije vidljiv. Na tom Macu uključite „Učini ovaj Mac vidljivim” u Postavke › Uređaji." + "value": "Novi panel (automatski raspored)" } }, "da": { "stringUnit": { "state": "translated", - "value": "Denne Mac kan ikke findes. Slå “Gør denne Mac synlig” til på den Mac under Indstillinger › Enheder." + "value": "Nyt panel (automatisk layout)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuer Bereich (automatisches Layout)" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "New Pane (Auto Layout)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nuevo panel (diseño automático)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau panneau (disposition automatique)" } }, "it": { "stringUnit": { "state": "translated", - "value": "Questo Mac non è individuabile. Su quel Mac, attiva “Rendi individuabile questo Mac” in Impostazioni › Dispositivi." + "value": "Nuovo pannello (layout automatico)" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "Mac នេះមិនអាចរកឃើញបានទេ។ នៅលើ Mac នោះ សូមបើក “ធ្វើឲ្យ Mac នេះអាចរកឃើញ” ក្នុងការកំណត់ › ឧបករណ៍។" + "value": "新しいペイン(自動レイアウト)" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 패널 (자동 레이아웃)" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Denne Macen er ikke synlig. Slå på «Gjør denne Macen synlig» på den Macen under Innstillinger › Enheter." + "value": "Nytt panel (automatisk oppsett)" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Ten Mac nie jest wykrywalny. Na tym Macu włącz „Uczyń ten Mac wykrywalnym” w Ustawienia › Urządzenia." + "value": "Nowy panel (układ automatyczny)" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Este Mac não está detectável. Nesse Mac, ative “Tornar este Mac detectável” em Ajustes › Dispositivos." + "value": "Novo Painel (Layout Automático)" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Этот Mac недоступен для обнаружения. На том Mac включите «Сделать этот Mac доступным для обнаружения» в разделе Настройки › Устройства." + "value": "Новая панель (автоматическая раскладка)" } }, "th": { "stringUnit": { "state": "translated", - "value": "Mac เครื่องนี้ไม่สามารถค้นพบได้ บน Mac เครื่องนั้น ให้เปิด “ทำให้ Mac เครื่องนี้ค้นพบได้” ในการตั้งค่า › อุปกรณ์" + "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu Mac keşfedilebilir değil. O Mac’te Ayarlar › Cihazlar bölümünden “Bu Mac’i keşfedilebilir yap” seçeneğini açın." + "value": "Yeni Bölme (Otomatik Düzen)" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Цей Mac недоступний для виявлення. На тому Mac увімкніть «Зробити цей Mac доступним для виявлення» у розділі Налаштування › Пристрої." + "value": "Нова панель (автоматичне розташування)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建窗格(自动布局)" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增窗格(自動佈局)" } } } @@ -568864,855 +569831,6 @@ } } }, - "machines.requiresPro.upgrade.short": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Upgrade" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "ترقية" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Upgraden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Actualizar" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mettre à niveau" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "アップグレード" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "업그레이드" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "升级" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "升級" - } - } - } - }, - "machines.sessionRejected.signInAgain.short": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Sign In" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تسجيل الدخول" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Anmelden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Iniciar sesión" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Se reconnecter" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "再サインイン" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "다시 로그인" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新登录" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重新登入" - } - } - } - }, - "machines.requiresPro.stale": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Cloud machines need cmux Pro — showing last known" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تتطلب الأجهزة السحابية إصدار cmux Pro — يتم عرض آخر حالة معروفة" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Cloud-Maschinen erfordern cmux Pro – zeige letzten bekannten Stand" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Las máquinas en la nube requieren cmux Pro — se muestra el último estado conocido" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Les machines Cloud nécessitent cmux Pro — affichage des dernières informations connues" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud マシンには cmux Pro が必要です — 最後の状態を表示中" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "클라우드 머신을 사용하려면 cmux Pro가 필요함 — 마지막으로 확인된 상태 표시 중" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "云端机器需要 cmux Pro —— 显示的是最后已知状态" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "雲端機器需要 cmux Pro — 顯示上次已知的狀態" - } - } - } - }, - "machines.sessionRejected.stale": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Sign-in needs a refresh — showing last known" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يحتاج تسجيل الدخول إلى تحديث — يتم عرض آخر حالة معروفة" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Anmeldung muss aktualisiert werden – zeige letzten bekannten Stand" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Es necesario actualizar el inicio de sesión — se muestra el último estado conocido" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "La connexion doit être actualisée — affichage des dernières informations connues" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "再サインインが必要です — 最後の状態を表示中" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "로그인 갱신 필요 — 마지막으로 확인된 상태 표시 중" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "登录状态需要刷新 —— 显示的是最后已知状态" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "登入狀態需要重新整理 — 顯示上次已知的狀態" - } - } - } - }, - "cli.auth.status.teamNotLoaded": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": " team: not loaded" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": " チーム: 未読み込み" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": " 团队: 未加载" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": " Team: nicht geladen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": " équipe: non chargée" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": " الفريق: لم يتم تحميله" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": " equipo: no cargado" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": " 團隊: 未載入" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": " 팀: 로드되지 않음" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": " tim: nije učitan" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": " team: ikke indlæst" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": " team: non caricato" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": " ក្រុម៖ មិនទាន់បានផ្ទុក" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": " team: ikke lastet" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": " zespół: nie załadowano" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": " equipe: não carregada" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": " команда: не загружена" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": " ทีม: ยังไม่ได้โหลด" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": " ekip: yüklenmedi" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": " команда: не завантажена" - } - } - } - }, - "cli.auth.status.teamNotLoadedRetrying": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": " team: not loaded (retrying)" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": " チーム: 未読み込み(再試行中)" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": " 团队: 未加载(正在重试)" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": " Team: nicht geladen (wird erneut versucht)" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": " équipe: non chargée (nouvel essai)" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": " الفريق: لم يتم تحميله (جارٍ إعادة المحاولة)" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": " equipo: no cargado (reintentando)" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": " 團隊: 未載入(重試中)" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": " 팀: 로드되지 않음 (재시도 중)" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": " tim: nije učitan (ponovni pokušaj)" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": " team: ikke indlæst (prøver igen)" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": " team: non caricato (nuovo tentativo)" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": " ក្រុម៖ មិនទាន់បានផ្ទុក (កំពុងព្យាយាមម្ដងទៀត)" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": " team: ikke lastet (prøver igjen)" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": " zespół: nie załadowano (ponawianie próby)" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": " equipe: não carregada (tentando novamente)" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": " команда: не загружена (повторная попытка)" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": " ทีม: ยังไม่ได้โหลด (กำลังลองอีกครั้ง)" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": " ekip: yüklenmedi (yeniden deneniyor)" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": " команда: не завантажена (повторна спроба)" - } - } - } - }, - "mobile.pairing.error.teamUnavailable": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Could not load your cmux team. Check your internet connection, then try again." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux チームを読み込めませんでした。インターネット接続を確認してから、もう一度お試しください。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法加载你的 cmux 团队。请检查网络连接,然后重试。" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Dein cmux-Team konnte nicht geladen werden. Prüfe deine Internetverbindung und versuche es erneut." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Impossible de charger votre équipe cmux. Vérifiez votre connexion Internet, puis réessayez." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذّر تحميل فريق cmux الخاص بك. تحقّق من اتصالك بالإنترنت، ثم حاول مرة أخرى." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se pudo cargar tu equipo de cmux. Comprueba tu conexión a internet e inténtalo de nuevo." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "無法載入你的 cmux 團隊。請檢查網路連線,然後再試一次。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux 팀을 불러올 수 없습니다. 인터넷 연결을 확인한 후 다시 시도하세요." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Nije moguće učitati vaš cmux tim. Provjerite internetsku vezu i pokušajte ponovo." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Din cmux-team kunne ikke indlæses. Kontrollér din internetforbindelse, og prøv igen." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Impossibile caricare il tuo team cmux. Controlla la connessione Internet e riprova." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "មិនអាចផ្ទុកក្រុម cmux របស់អ្នកបានទេ។ សូមពិនិត្យការតភ្ជាប់អ៊ីនធឺណិត ហើយព្យាយាមម្ដងទៀត។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kunne ikke laste inn cmux-teamet ditt. Kontroller internettilkoblingen, og prøv igjen." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Nie udało się załadować Twojego zespołu cmux. Sprawdź połączenie z internetem i spróbuj ponownie." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Não foi possível carregar sua equipe cmux. Verifique sua conexão com a internet e tente novamente." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Не удалось загрузить вашу команду cmux. Проверьте подключение к интернету и попробуйте ещё раз." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ไม่สามารถโหลดทีม cmux ของคุณได้ ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองอีกครั้ง" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux ekibiniz yüklenemedi. İnternet bağlantınızı kontrol edip tekrar deneyin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Не вдалося завантажити вашу команду cmux. Перевірте підключення до інтернету й спробуйте ще раз." - } - } - } -}, - "command.newPaneAutoLayout.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "لوحة جديدة (تخطيط تلقائي)" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Novi panel (automatski raspored)" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Nyt panel (automatisk layout)" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Neuer Bereich (automatisches Layout)" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "New Pane (Auto Layout)" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Nuevo panel (diseño automático)" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Nouveau panneau (disposition automatique)" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Nuovo pannello (layout automatico)" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "新しいペイン(自動レイアウト)" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "새 패널 (자동 레이아웃)" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Nytt panel (automatisk oppsett)" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Nowy panel (układ automatyczny)" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Novo Painel (Layout Automático)" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Новая панель (автоматическая раскладка)" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Yeni Bölme (Otomatik Düzen)" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Нова панель (автоматичне розташування)" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建窗格(自动布局)" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "新增窗格(自動佈局)" - } - } - } - }, - "shortcut.newPaneAutoLayout.label": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "لوحة جديدة (تخطيط تلقائي)" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Novi panel (automatski raspored)" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Nyt panel (automatisk layout)" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Neuer Bereich (automatisches Layout)" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "New Pane (Auto Layout)" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Nuevo panel (diseño automático)" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Nouveau panneau (disposition automatique)" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Nuovo pannello (layout automatico)" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "新しいペイン(自動レイアウト)" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "새 패널 (자동 레이아웃)" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Nytt panel (automatisk oppsett)" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Nowy panel (układ automatyczny)" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Novo Painel (Layout Automático)" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Новая панель (автоматическая раскладка)" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Yeni Bölme (Otomatik Düzen)" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Нова панель (автоматичне розташування)" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建窗格(自动布局)" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "新增窗格(自動佈局)" - } - } - } - }, "cli.restoreSession.help": { "extractionState": "manual", "localizations": { @@ -571247,124 +571365,6 @@ } } }, - "settings.notifications.soundWhenFocused.subtitle": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Play the notification sound even when the pane that notified is already focused. When off, a focused pane shows its ring without a sound." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "شغّل صوت الإشعار حتى عندما تكون اللوحة التي أرسلت الإشعار في موضع التركيز بالفعل. عند إيقافه، تعرض اللوحة المُركَّز عليها حلقتها دون صوت." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Den Benachrichtigungston auch abspielen, wenn der benachrichtigende Bereich bereits fokussiert ist. Wenn aus, zeigt ein fokussierter Bereich seinen Ring ohne Ton." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reproduce el sonido de notificación incluso cuando el panel que notificó ya tiene el foco. Si está desactivado, un panel con foco muestra su anillo sin sonido." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Jouer le son de notification même lorsque le panneau à l’origine de la notification a déjà le focus. Désactivé, un panneau actif affiche son anneau sans son." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "通知を送ったペインがすでにフォーカスされている場合でも通知音を鳴らします。オフの場合、フォーカス中のペインは音を鳴らさずにリングを表示します。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "알림을 보낸 패널이 이미 포커스된 상태여도 알림음을 재생합니다. 끄면 포커스된 패널은 소리 없이 링만 표시합니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "即使发出通知的窗格已处于焦点状态,也播放通知声音。关闭时,处于焦点的窗格会显示光环但不发出声音。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "即使發出通知的窗格已在焦點中,也播放通知聲音。關閉時,焦點中的窗格會顯示光環但不發出聲音。" - } - } - } - }, - "settings.notifications.soundWhenFocused.title": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Sound for Focused Pane" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "صوت للوحة المُركَّز عليها" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Ton für fokussierten Bereich" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Sonido para el panel con foco" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Son pour le panneau actif" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "フォーカス中のペインの通知音" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "포커스된 패널 소리" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "焦点窗格的声音" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "焦點窗格的聲音" - } - } - } - }, "agent.claude.stopFailure.body.generic": { "extractionState": "manual", "localizations": { @@ -571955,6 +571955,124 @@ } } }, + "cli.focus.defaultHelp": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Default: focus when run interactively, stay in the background when run by an agent or script." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "既定: 対話的に実行した場合はフォーカスし、エージェントやスクリプトから実行した場合はバックグラウンドのままにします。" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standard: bei interaktiver Ausführung fokussieren, bei Ausführung durch einen Agenten oder ein Skript im Hintergrund bleiben." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut : prend le focus en exécution interactive, reste en arrière-plan quand un agent ou un script l’exécute." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الافتراضي: التركيز عند التشغيل التفاعلي، والبقاء في الخلفية عند التشغيل بواسطة وكيل أو برنامج نصي." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Predeterminado: enfoca al ejecutarse de forma interactiva y se queda en segundo plano cuando lo ejecuta un agente o un script." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "預設:互動執行時取得焦點;由代理程式或指令稿執行時保持在背景。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "默认:交互式运行时获得焦点;由代理或脚本运行时保持在后台。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "기본값: 대화형으로 실행하면 포커스를 가져오고, 에이전트나 스크립트가 실행하면 백그라운드에 머뭅니다." + } + } + } + }, + "cli.help.openFocusFlags": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Focus:\n --focus Switch to the new workspace\n --no-focus Open it in the background" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "フォーカス:\n --focus 新しいワークスペースに切り替えます\n --no-focus バックグラウンドで開きます" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fokus:\n --focus Zum neuen Workspace wechseln\n --no-focus Im Hintergrund öffnen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Focus :\n --focus Bascule vers le nouvel espace de travail\n --no-focus L’ouvre en arrière-plan" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "التركيز:\n --focus ينتقل إلى مساحة العمل الجديدة\n --no-focus يفتحها في الخلفية" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Foco:\n --focus Cambia al nuevo espacio de trabajo\n --no-focus Lo abre en segundo plano" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "焦點:\n --focus 切換到新的工作區\n --no-focus 在背景開啟" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "焦点:\n --focus 切换到新的工作区\n --no-focus 在后台打开" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "포커스:\n --focus 새 워크스페이스로 전환합니다\n --no-focus 백그라운드에서 엽니다" + } + } + } + }, "settings.app.reorderOnNotification.mode.agentActivity": { "extractionState": "manual", "localizations": { @@ -572431,6 +572549,124 @@ } } }, + "settings.notifications.soundWhenFocused.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Play the notification sound even when the pane that notified is already focused. When off, a focused pane shows its ring without a sound." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "شغّل صوت الإشعار حتى عندما تكون اللوحة التي أرسلت الإشعار في موضع التركيز بالفعل. عند إيقافه، تعرض اللوحة المُركَّز عليها حلقتها دون صوت." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Den Benachrichtigungston auch abspielen, wenn der benachrichtigende Bereich bereits fokussiert ist. Wenn aus, zeigt ein fokussierter Bereich seinen Ring ohne Ton." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reproduce el sonido de notificación incluso cuando el panel que notificó ya tiene el foco. Si está desactivado, un panel con foco muestra su anillo sin sonido." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer le son de notification même lorsque le panneau à l’origine de la notification a déjà le focus. Désactivé, un panneau actif affiche son anneau sans son." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "通知を送ったペインがすでにフォーカスされている場合でも通知音を鳴らします。オフの場合、フォーカス中のペインは音を鳴らさずにリングを表示します。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "알림을 보낸 패널이 이미 포커스된 상태여도 알림음을 재생합니다. 끄면 포커스된 패널은 소리 없이 링만 표시합니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "即使发出通知的窗格已处于焦点状态,也播放通知声音。关闭时,处于焦点的窗格会显示光环但不发出声音。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "即使發出通知的窗格已在焦點中,也播放通知聲音。關閉時,焦點中的窗格會顯示光環但不發出聲音。" + } + } + } + }, + "settings.notifications.soundWhenFocused.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sound for Focused Pane" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "صوت للوحة المُركَّز عليها" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ton für fokussierten Bereich" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sonido para el panel con foco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Son pour le panneau actif" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "フォーカス中のペインの通知音" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "포커스된 패널 소리" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "焦点窗格的声音" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "焦點窗格的聲音" + } + } + } + }, "cli.restore.notice.routedLauncherUnavailable": { "extractionState": "manual", "localizations": { @@ -572490,179 +572726,297 @@ } } }, - "sidebar.extensions.action.noSpaceForPane": { + "machines.requiresPro.upgrade.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No space for new pane" + "value": "Upgrade" } }, "ar": { "stringUnit": { "state": "translated", - "value": "لا توجد مساحة للوحة جديدة" + "value": "ترقية" } }, "de": { "stringUnit": { "state": "translated", - "value": "Kein Platz für einen neuen Bereich" + "value": "Upgraden" } }, "es": { "stringUnit": { "state": "translated", - "value": "No hay espacio para un panel nuevo" + "value": "Actualizar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Pas de place pour un nouveau panneau" + "value": "Mettre à niveau" } }, "ja": { "stringUnit": { "state": "translated", - "value": "新しいペインを置く場所がありません" + "value": "アップグレード" } }, "ko": { "stringUnit": { "state": "translated", - "value": "새 창을 위한 공간이 없습니다" + "value": "업그레이드" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "没有空间容纳新窗格" + "value": "升级" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "沒有空間容納新窗格" + "value": "升級" } } } }, - "cli.focus.defaultHelp": { + "machines.sessionRejected.signInAgain.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Default: focus when run interactively, stay in the background when run by an agent or script." + "value": "Sign In" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "既定: 対話的に実行した場合はフォーカスし、エージェントやスクリプトから実行した場合はバックグラウンドのままにします。" + "value": "تسجيل الدخول" } }, "de": { "stringUnit": { "state": "translated", - "value": "Standard: bei interaktiver Ausführung fokussieren, bei Ausführung durch einen Agenten oder ein Skript im Hintergrund bleiben." + "value": "Anmelden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iniciar sesión" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Par défaut : prend le focus en exécution interactive, reste en arrière-plan quand un agent ou un script l’exécute." + "value": "Se reconnecter" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再サインイン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다시 로그인" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重新登录" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新登入" + } + } + } + }, + "machines.requiresPro.stale": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud machines need cmux Pro — showing last known" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الافتراضي: التركيز عند التشغيل التفاعلي، والبقاء في الخلفية عند التشغيل بواسطة وكيل أو برنامج نصي." + "value": "تتطلب الأجهزة السحابية إصدار cmux Pro — يتم عرض آخر حالة معروفة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Maschinen erfordern cmux Pro – zeige letzten bekannten Stand" } }, "es": { "stringUnit": { "state": "translated", - "value": "Predeterminado: enfoca al ejecutarse de forma interactiva y se queda en segundo plano cuando lo ejecuta un agente o un script." + "value": "Las máquinas en la nube requieren cmux Pro — se muestra el último estado conocido" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "預設:互動執行時取得焦點;由代理程式或指令稿執行時保持在背景。" + "value": "Les machines Cloud nécessitent cmux Pro — affichage des dernières informations connues" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "默认:交互式运行时获得焦点;由代理或脚本运行时保持在后台。" + "value": "Cloud マシンには cmux Pro が必要です — 最後の状態を表示中" } }, "ko": { "stringUnit": { "state": "translated", - "value": "기본값: 대화형으로 실행하면 포커스를 가져오고, 에이전트나 스크립트가 실행하면 백그라운드에 머뭅니다." + "value": "클라우드 머신을 사용하려면 cmux Pro가 필요함 — 마지막으로 확인된 상태 표시 중" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端机器需要 cmux Pro —— 显示的是最后已知状态" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端機器需要 cmux Pro — 顯示上次已知的狀態" } } } }, - "cli.help.openFocusFlags": { + "machines.sessionRejected.stale": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Focus:\n --focus Switch to the new workspace\n --no-focus Open it in the background" + "value": "Sign-in needs a refresh — showing last known" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "フォーカス:\n --focus 新しいワークスペースに切り替えます\n --no-focus バックグラウンドで開きます" + "value": "يحتاج تسجيل الدخول إلى تحديث — يتم عرض آخر حالة معروفة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Fokus:\n --focus Zum neuen Workspace wechseln\n --no-focus Im Hintergrund öffnen" + "value": "Anmeldung muss aktualisiert werden – zeige letzten bekannten Stand" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Es necesario actualizar el inicio de sesión — se muestra el último estado conocido" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Focus :\n --focus Bascule vers le nouvel espace de travail\n --no-focus L’ouvre en arrière-plan" + "value": "La connexion doit être actualisée — affichage des dernières informations connues" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再サインインが必要です — 最後の状態を表示中" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "로그인 갱신 필요 — 마지막으로 확인된 상태 표시 중" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "登录状态需要刷新 —— 显示的是最后已知状态" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "登入狀態需要重新整理 — 顯示上次已知的狀態" + } + } + } + }, + "sidebar.extensions.action.noSpaceForPane": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No space for new pane" } }, "ar": { "stringUnit": { "state": "translated", - "value": "التركيز:\n --focus ينتقل إلى مساحة العمل الجديدة\n --no-focus يفتحها في الخلفية" + "value": "لا توجد مساحة للوحة جديدة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Platz für einen neuen Bereich" } }, "es": { "stringUnit": { "state": "translated", - "value": "Foco:\n --focus Cambia al nuevo espacio de trabajo\n --no-focus Lo abre en segundo plano" + "value": "No hay espacio para un panel nuevo" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "焦點:\n --focus 切換到新的工作區\n --no-focus 在背景開啟" + "value": "Pas de place pour un nouveau panneau" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "焦点:\n --focus 切换到新的工作区\n --no-focus 在后台打开" + "value": "新しいペインを置く場所がありません" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포커스:\n --focus 새 워크스페이스로 전환합니다\n --no-focus 백그라운드에서 엽니다" + "value": "새 창을 위한 공간이 없습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "没有空间容纳新窗格" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "沒有空間容納新窗格" } } } @@ -581496,1061 +581850,1061 @@ } } }, - "devices.incoming.confirm.enable": { + "terminal.clipboardReadConfirmation.deny": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Make Discoverable" + "value": "Deny" } }, "ar": { "stringUnit": { "state": "translated", - "value": "جعل الجهاز قابلاً للاكتشاف" + "value": "رفض" } }, "de": { "stringUnit": { "state": "translated", - "value": "Auffindbar machen" + "value": "Ablehnen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Hacer detectable" + "value": "Denegar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Rendre détectable" + "value": "Refuser" } }, "ja": { "stringUnit": { "state": "translated", - "value": "検出可能にする" + "value": "拒否" } }, "ko": { "stringUnit": { "state": "translated", - "value": "검색 허용" + "value": "거부" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "允许发现" + "value": "拒绝" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "允許探索" + "value": "拒絕" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Učini dostupnim" + "value": "Odbij" } }, "da": { "stringUnit": { "state": "translated", - "value": "Gør synlig" + "value": "Afvis" } }, "it": { "stringUnit": { "state": "translated", - "value": "Rendi individuabile" + "value": "Nega" } }, "km": { "stringUnit": { "state": "translated", - "value": "ធ្វើឲ្យអាចរកឃើញ" + "value": "បដិសេធ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Gjør denne Mac-en synlig" + "value": "Avslå" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Włącz wykrywanie" + "value": "Odmów" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Tornar detectável" + "value": "Negar" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Сделать доступным для обнаружения" + "value": "Запретить" } }, "th": { "stringUnit": { "state": "translated", - "value": "ทำให้ Mac เครื่องนี้ค้นพบได้" + "value": "ปฏิเสธ" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Keşfedilebilir yap" + "value": "Reddet" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Зробити цей Mac видимим" + "value": "Заборонити" } } } }, - "devices.incoming.confirm.message": { + "terminal.clipboardReadConfirmation.message": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Other Macs signed in to your cmux account may discover this Mac and connect to its workspaces. Do you want to continue?" + "value": "A program in this terminal wants to read your clipboard. Allow it only if you trust the program." } }, "ar": { "stringUnit": { "state": "translated", - "value": "قد تكتشف أجهزة Mac الأخرى التي سجّلت الدخول إلى حساب cmux هذا الـ Mac وتتصل بمساحات العمل فيه. هل تريد المتابعة؟" + "value": "يريد برنامج في هذه الطرفية قراءة الحافظة. اسمح بذلك فقط إذا كنت تثق بالبرنامج." } }, "de": { "stringUnit": { "state": "translated", - "value": "Andere Macs, die bei deinem cmux-Konto angemeldet sind, können diesen Mac finden und sich mit seinen Arbeitsbereichen verbinden. Möchtest du fortfahren?" + "value": "Ein Programm in diesem Terminal möchte deine Zwischenablage lesen. Erlaube es nur, wenn du dem Programm vertraust." } }, "es": { "stringUnit": { "state": "translated", - "value": "Otros Mac que hayan iniciado sesión en tu cuenta de cmux podrán descubrir este Mac y conectarse a sus espacios de trabajo. ¿Quieres continuar?" + "value": "Un programa de esta terminal quiere leer tu portapapeles. Permítelo solo si confías en el programa." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les autres Mac connectés à votre compte cmux pourront détecter ce Mac et se connecter à ses espaces de travail. Voulez-vous continuer ?" + "value": "Un programme de ce terminal veut lire votre presse-papiers. Ne l’autorisez que si vous faites confiance à ce programme." } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmuxアカウントにサインインしているほかのMacから、このMacが検出され、ワークスペースに接続されることがあります。続けますか?" + "value": "このターミナルのプログラムがクリップボードを読み取ろうとしています。信頼できるプログラムの場合にのみ許可してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux 계정에 로그인한 다른 Mac에서 이 Mac을 검색하고 작업 공간에 연결할 수 있습니다. 계속하시겠습니까?" + "value": "이 터미널의 프로그램이 클립보드를 읽으려고 합니다. 프로그램을 신뢰할 수 있을 때만 허용하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "登录你 cmux 帐户的其他 Mac 可能会发现此 Mac 并连接到其工作区。要继续吗?" + "value": "此终端中的程序想要读取你的剪贴板。只有在你信任该程序时才允许。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "登入您 cmux 帳戶的其他 Mac 可能會探索此 Mac 並連線至其工作區。要繼續嗎?" + "value": "此終端機中的程式想要讀取你的剪貼簿。只有在你信任該程式時才允許。" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Drugi Mac računari prijavljeni na vaš cmux nalog možda će otkriti ovaj Mac i povezati se s njegovim radnim prostorima. Želite li nastaviti?" + "value": "Program u ovom terminalu želi čitati vaš međuspremnik. Dozvolite samo ako vjerujete programu." } }, "da": { "stringUnit": { "state": "translated", - "value": "Andre Mac-computere, der er logget ind på din cmux-konto, kan muligvis finde denne Mac og oprette forbindelse til dens arbejdsområder. Vil du fortsætte?" + "value": "Et program i denne terminal vil læse din udklipsholder. Tillad det kun, hvis du har tillid til programmet." } }, "it": { "stringUnit": { "state": "translated", - "value": "Gli altri Mac che hanno effettuato l’accesso al tuo account cmux potrebbero rilevare questo Mac e connettersi ai suoi spazi di lavoro. Vuoi continuare?" + "value": "Un programma in questo terminale vuole leggere gli appunti. Consentilo solo se ti fidi del programma." } }, "km": { "stringUnit": { "state": "translated", - "value": "Mac ផ្សេងទៀតដែលបានចូលគណនី cmux របស់អ្នក អាចរកឃើញ Mac នេះ និងភ្ជាប់ទៅកាន់ workspace របស់វា។ តើអ្នកចង់បន្តឬទេ?" + "value": "កម្មវិធីក្នុងស្ថានីយនេះចង់អានក្ដារតម្បៀតខ្ទាស់របស់អ្នក។ អនុញ្ញាតតែបើអ្នកទុកចិត្តកម្មវិធីនេះ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Andre Mac-maskiner som er logget på cmux-kontoen din, kan finne denne Mac-en og koble til arbeidsområdene. Vil du fortsette?" + "value": "Et program i denne terminalen vil lese utklippstavlen din. Tillat det bare hvis du stoler på programmet." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Inne Maki zalogowane na Twoje konto cmux mogą wykryć tego Maca i połączyć się z jego obszarami roboczymi. Czy chcesz kontynuować?" + "value": "Program w tym terminalu chce odczytać schowek. Zezwól tylko, jeśli ufasz temu programowi." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Outros Macs conectados à sua conta cmux poderão descobrir este Mac e se conectar aos seus espaços de trabalho. Deseja continuar?" + "value": "Um programa neste terminal quer ler sua área de transferência. Permita somente se você confiar no programa." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Другие Mac, вошедшие в вашу учётную запись cmux, могут обнаружить этот Mac и подключиться к его рабочим пространствам. Продолжить?" + "value": "Программа в этом терминале хочет прочитать буфер обмена. Разрешайте только программам, которым доверяете." } }, "th": { "stringUnit": { "state": "translated", - "value": "Mac เครื่องอื่นที่ลงชื่อเข้าใช้บัญชี cmux ของคุณอาจค้นพบ Mac เครื่องนี้และเชื่อมต่อกับพื้นที่ทำงานของเครื่องนี้ คุณต้องการดำเนินการต่อหรือไม่" + "value": "โปรแกรมในเทอร์มินัลนี้ต้องการอ่านคลิปบอร์ดของคุณ อนุญาตเฉพาะเมื่อคุณเชื่อถือโปรแกรมนี้" } }, "tr": { "stringUnit": { "state": "translated", - "value": "cmux hesabınızda oturum açmış diğer Mac’ler bu Mac’i keşfedebilir ve çalışma alanlarına bağlanabilir. Devam etmek istiyor musunuz?" + "value": "Bu terminaldeki bir program panonuzu okumak istiyor. Yalnızca programa güveniyorsanız izin verin." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Інші Mac, на яких виконано вхід у ваш обліковий запис cmux, можуть виявити цей Mac і підключитися до його робочих просторів. Продовжити?" + "value": "Програма в цьому терміналі хоче прочитати ваш буфер обміну. Дозволяйте, лише якщо довіряєте програмі." } } } }, - "devices.incoming.confirm.title": { + "terminal.clipboardReadConfirmation.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Make this Mac discoverable?" + "value": "Allow Clipboard Access?" } }, "ar": { "stringUnit": { "state": "translated", - "value": "هل تريد جعل جهاز Mac هذا قابلاً للاكتشاف؟" + "value": "السماح بالوصول إلى الحافظة؟" } }, "de": { "stringUnit": { "state": "translated", - "value": "Diesen Mac auffindbar machen?" + "value": "Zugriff auf die Zwischenablage erlauben?" } }, "es": { "stringUnit": { "state": "translated", - "value": "¿Hacer que este Mac sea detectable?" + "value": "¿Permitir el acceso al portapapeles?" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Rendre ce Mac détectable ?" + "value": "Autoriser l’accès au presse-papiers ?" } }, "ja": { "stringUnit": { "state": "translated", - "value": "このMacを検出可能にしますか?" + "value": "クリップボードへのアクセスを許可しますか?" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 Mac을 검색 가능하게 할까요?" + "value": "클립보드 접근을 허용하시겠습니까?" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "允许发现此 Mac 吗?" + "value": "允许访问剪贴板?" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "要允許探索此 Mac 嗎?" + "value": "允許存取剪貼簿?" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Učiniti ovaj Mac dostupnim?" + "value": "Dozvoliti pristup međuspremniku?" } }, "da": { "stringUnit": { "state": "translated", - "value": "Gør denne Mac synlig?" + "value": "Tillad adgang til udklipsholderen?" } }, "it": { "stringUnit": { "state": "translated", - "value": "Rendere individuabile questo Mac?" + "value": "Consentire l'accesso agli appunti?" } }, "km": { "stringUnit": { "state": "translated", - "value": "ធ្វើឲ្យ Mac នេះអាចរកឃើញ?" + "value": "អនុញ្ញាតឱ្យចូលប្រើក្ដារតម្បៀតខ្ទាស់?" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Gjøre denne Mac-en synlig?" + "value": "Tillat tilgang til utklippstavlen?" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Uwidocznić tego Maca?" + "value": "Zezwolić na dostęp do schowka?" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Tornar este Mac detectável?" + "value": "Permitir acesso à área de transferência?" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Сделать этот Mac доступным для обнаружения?" + "value": "Разрешить доступ к буферу обмена?" } }, "th": { "stringUnit": { "state": "translated", - "value": "ทำให้ Mac เครื่องนี้ค้นพบได้หรือไม่" + "value": "อนุญาตให้เข้าถึงคลิปบอร์ดหรือไม่?" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu Mac keşfedilebilir yapılsın mı?" + "value": "Panoya erişime izin verilsin mi?" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Зробити цей Mac доступним для виявлення?" - } - } - } - }, - "cloudTree.device.status.connecting": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Connecting…" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ الاتصال…" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Wird verbunden…" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Conectando…" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Connexion…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "接続中…" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "연결 중…" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在连接…" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "正在連線…" + "value": "Дозволити доступ до буфера обміну?" } } } }, - "terminal.clipboardReadConfirmation.deny": { + "notification.remoteRelay.hostFallback": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Deny" + "value": "Remote host" } }, "ar": { "stringUnit": { "state": "translated", - "value": "رفض" + "value": "مضيف بعيد" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ablehnen" + "value": "Remote-Host" } }, "es": { "stringUnit": { "state": "translated", - "value": "Denegar" + "value": "Host remoto" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Refuser" + "value": "Hôte distant" } }, "ja": { "stringUnit": { "state": "translated", - "value": "拒否" + "value": "リモートホスト" } }, "ko": { "stringUnit": { "state": "translated", - "value": "거부" + "value": "원격 호스트" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "拒绝" + "value": "远程主机" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "拒絕" + "value": "遠端主機" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Odbij" + "value": "Udaljeni host" } }, "da": { "stringUnit": { "state": "translated", - "value": "Afvis" + "value": "Fjernvært" } }, "it": { "stringUnit": { "state": "translated", - "value": "Nega" + "value": "Host remoto" } }, "km": { "stringUnit": { "state": "translated", - "value": "បដិសេធ" + "value": "ម៉ាស៊ីនពីចម្ងាយ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Avslå" + "value": "Ekstern vert" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Odmów" + "value": "Zdalny host" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Negar" + "value": "Host remoto" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Запретить" + "value": "Удалённый хост" } }, "th": { "stringUnit": { "state": "translated", - "value": "ปฏิเสธ" + "value": "โฮสต์ระยะไกล" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Reddet" + "value": "Uzak ana bilgisayar" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Заборонити" + "value": "Віддалений хост" } } } }, - "terminal.clipboardReadConfirmation.message": { + "notification.remoteRelay.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "A program in this terminal wants to read your clipboard. Allow it only if you trust the program." + "value": "%1$@: %2$@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يريد برنامج في هذه الطرفية قراءة الحافظة. اسمح بذلك فقط إذا كنت تثق بالبرنامج." + "value": "%1$@: %2$@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ein Programm in diesem Terminal möchte deine Zwischenablage lesen. Erlaube es nur, wenn du dem Programm vertraust." + "value": "%1$@: %2$@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Un programa de esta terminal quiere leer tu portapapeles. Permítelo solo si confías en el programa." + "value": "%1$@: %2$@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Un programme de ce terminal veut lire votre presse-papiers. Ne l’autorisez que si vous faites confiance à ce programme." + "value": "%1$@: %2$@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "このターミナルのプログラムがクリップボードを読み取ろうとしています。信頼できるプログラムの場合にのみ許可してください。" + "value": "%1$@: %2$@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 터미널의 프로그램이 클립보드를 읽으려고 합니다. 프로그램을 신뢰할 수 있을 때만 허용하세요." + "value": "%1$@: %2$@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此终端中的程序想要读取你的剪贴板。只有在你信任该程序时才允许。" + "value": "%1$@: %2$@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此終端機中的程式想要讀取你的剪貼簿。只有在你信任該程式時才允許。" + "value": "%1$@: %2$@" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Program u ovom terminalu želi čitati vaš međuspremnik. Dozvolite samo ako vjerujete programu." + "value": "%1$@: %2$@" } }, "da": { "stringUnit": { "state": "translated", - "value": "Et program i denne terminal vil læse din udklipsholder. Tillad det kun, hvis du har tillid til programmet." + "value": "%1$@: %2$@" } }, "it": { "stringUnit": { "state": "translated", - "value": "Un programma in questo terminale vuole leggere gli appunti. Consentilo solo se ti fidi del programma." + "value": "%1$@: %2$@" } }, "km": { "stringUnit": { "state": "translated", - "value": "កម្មវិធីក្នុងស្ថានីយនេះចង់អានក្ដារតម្បៀតខ្ទាស់របស់អ្នក។ អនុញ្ញាតតែបើអ្នកទុកចិត្តកម្មវិធីនេះ។" + "value": "%1$@: %2$@" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Et program i denne terminalen vil lese utklippstavlen din. Tillat det bare hvis du stoler på programmet." + "value": "%1$@: %2$@" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Program w tym terminalu chce odczytać schowek. Zezwól tylko, jeśli ufasz temu programowi." + "value": "%1$@: %2$@" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Um programa neste terminal quer ler sua área de transferência. Permita somente se você confiar no programa." + "value": "%1$@: %2$@" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Программа в этом терминале хочет прочитать буфер обмена. Разрешайте только программам, которым доверяете." + "value": "%1$@: %2$@" } }, "th": { "stringUnit": { "state": "translated", - "value": "โปรแกรมในเทอร์มินัลนี้ต้องการอ่านคลิปบอร์ดของคุณ อนุญาตเฉพาะเมื่อคุณเชื่อถือโปรแกรมนี้" + "value": "%1$@: %2$@" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu terminaldeki bir program panonuzu okumak istiyor. Yalnızca programa güveniyorsanız izin verin." + "value": "%1$@: %2$@" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Програма в цьому терміналі хоче прочитати ваш буфер обміну. Дозволяйте, лише якщо довіряєте програмі." + "value": "%1$@: %2$@" } } } }, - "terminal.clipboardReadConfirmation.title": { + "devices.incoming.confirm.enable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Allow Clipboard Access?" + "value": "Make Discoverable" } }, "ar": { "stringUnit": { "state": "translated", - "value": "السماح بالوصول إلى الحافظة؟" + "value": "جعل الجهاز قابلاً للاكتشاف" } }, "de": { "stringUnit": { "state": "translated", - "value": "Zugriff auf die Zwischenablage erlauben?" + "value": "Auffindbar machen" } }, "es": { "stringUnit": { "state": "translated", - "value": "¿Permitir el acceso al portapapeles?" + "value": "Hacer detectable" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Autoriser l’accès au presse-papiers ?" + "value": "Rendre détectable" } }, "ja": { "stringUnit": { "state": "translated", - "value": "クリップボードへのアクセスを許可しますか?" + "value": "検出可能にする" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클립보드 접근을 허용하시겠습니까?" + "value": "검색 허용" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "允许访问剪贴板?" + "value": "允许发现" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "允許存取剪貼簿?" + "value": "允許探索" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Dozvoliti pristup međuspremniku?" + "value": "Učini dostupnim" } }, "da": { "stringUnit": { "state": "translated", - "value": "Tillad adgang til udklipsholderen?" + "value": "Gør synlig" } }, "it": { "stringUnit": { "state": "translated", - "value": "Consentire l'accesso agli appunti?" + "value": "Rendi individuabile" } }, "km": { "stringUnit": { "state": "translated", - "value": "អនុញ្ញាតឱ្យចូលប្រើក្ដារតម្បៀតខ្ទាស់?" + "value": "ធ្វើឲ្យអាចរកឃើញ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Tillat tilgang til utklippstavlen?" + "value": "Gjør denne Mac-en synlig" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Zezwolić na dostęp do schowka?" + "value": "Włącz wykrywanie" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Permitir acesso à área de transferência?" + "value": "Tornar detectável" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Разрешить доступ к буферу обмена?" + "value": "Сделать доступным для обнаружения" } }, "th": { "stringUnit": { "state": "translated", - "value": "อนุญาตให้เข้าถึงคลิปบอร์ดหรือไม่?" + "value": "ทำให้ Mac เครื่องนี้ค้นพบได้" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Panoya erişime izin verilsin mi?" + "value": "Keşfedilebilir yap" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Дозволити доступ до буфера обміну?" + "value": "Зробити цей Mac видимим" } } } }, - "notification.remoteRelay.hostFallback": { + "devices.incoming.confirm.message": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Remote host" + "value": "Other Macs signed in to your cmux account may discover this Mac and connect to its workspaces. Do you want to continue?" } }, "ar": { "stringUnit": { "state": "translated", - "value": "مضيف بعيد" + "value": "قد تكتشف أجهزة Mac الأخرى التي سجّلت الدخول إلى حساب cmux هذا الـ Mac وتتصل بمساحات العمل فيه. هل تريد المتابعة؟" } }, "de": { "stringUnit": { "state": "translated", - "value": "Remote-Host" + "value": "Andere Macs, die bei deinem cmux-Konto angemeldet sind, können diesen Mac finden und sich mit seinen Arbeitsbereichen verbinden. Möchtest du fortfahren?" } }, "es": { "stringUnit": { "state": "translated", - "value": "Host remoto" + "value": "Otros Mac que hayan iniciado sesión en tu cuenta de cmux podrán descubrir este Mac y conectarse a sus espacios de trabajo. ¿Quieres continuar?" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Hôte distant" + "value": "Les autres Mac connectés à votre compte cmux pourront détecter ce Mac et se connecter à ses espaces de travail. Voulez-vous continuer ?" } }, "ja": { "stringUnit": { "state": "translated", - "value": "リモートホスト" + "value": "cmuxアカウントにサインインしているほかのMacから、このMacが検出され、ワークスペースに接続されることがあります。続けますか?" } }, "ko": { "stringUnit": { "state": "translated", - "value": "원격 호스트" + "value": "cmux 계정에 로그인한 다른 Mac에서 이 Mac을 검색하고 작업 공간에 연결할 수 있습니다. 계속하시겠습니까?" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "远程主机" + "value": "登录你 cmux 帐户的其他 Mac 可能会发现此 Mac 并连接到其工作区。要继续吗?" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "遠端主機" + "value": "登入您 cmux 帳戶的其他 Mac 可能會探索此 Mac 並連線至其工作區。要繼續嗎?" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Udaljeni host" + "value": "Drugi Mac računari prijavljeni na vaš cmux nalog možda će otkriti ovaj Mac i povezati se s njegovim radnim prostorima. Želite li nastaviti?" } }, "da": { "stringUnit": { "state": "translated", - "value": "Fjernvært" + "value": "Andre Mac-computere, der er logget ind på din cmux-konto, kan muligvis finde denne Mac og oprette forbindelse til dens arbejdsområder. Vil du fortsætte?" } }, "it": { "stringUnit": { "state": "translated", - "value": "Host remoto" + "value": "Gli altri Mac che hanno effettuato l’accesso al tuo account cmux potrebbero rilevare questo Mac e connettersi ai suoi spazi di lavoro. Vuoi continuare?" } }, "km": { "stringUnit": { "state": "translated", - "value": "ម៉ាស៊ីនពីចម្ងាយ" + "value": "Mac ផ្សេងទៀតដែលបានចូលគណនី cmux របស់អ្នក អាចរកឃើញ Mac នេះ និងភ្ជាប់ទៅកាន់ workspace របស់វា។ តើអ្នកចង់បន្តឬទេ?" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Ekstern vert" + "value": "Andre Mac-maskiner som er logget på cmux-kontoen din, kan finne denne Mac-en og koble til arbeidsområdene. Vil du fortsette?" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Zdalny host" + "value": "Inne Maki zalogowane na Twoje konto cmux mogą wykryć tego Maca i połączyć się z jego obszarami roboczymi. Czy chcesz kontynuować?" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Host remoto" + "value": "Outros Macs conectados à sua conta cmux poderão descobrir este Mac e se conectar aos seus espaços de trabalho. Deseja continuar?" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Удалённый хост" + "value": "Другие Mac, вошедшие в вашу учётную запись cmux, могут обнаружить этот Mac и подключиться к его рабочим пространствам. Продолжить?" } }, "th": { "stringUnit": { "state": "translated", - "value": "โฮสต์ระยะไกล" + "value": "Mac เครื่องอื่นที่ลงชื่อเข้าใช้บัญชี cmux ของคุณอาจค้นพบ Mac เครื่องนี้และเชื่อมต่อกับพื้นที่ทำงานของเครื่องนี้ คุณต้องการดำเนินการต่อหรือไม่" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Uzak ana bilgisayar" + "value": "cmux hesabınızda oturum açmış diğer Mac’ler bu Mac’i keşfedebilir ve çalışma alanlarına bağlanabilir. Devam etmek istiyor musunuz?" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Віддалений хост" + "value": "Інші Mac, на яких виконано вхід у ваш обліковий запис cmux, можуть виявити цей Mac і підключитися до його робочих просторів. Продовжити?" } } } }, - "notification.remoteRelay.title": { + "devices.incoming.confirm.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Make this Mac discoverable?" } }, "ar": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "هل تريد جعل جهاز Mac هذا قابلاً للاكتشاف؟" } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Diesen Mac auffindbar machen?" } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "¿Hacer que este Mac sea detectable?" } }, "fr": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Rendre ce Mac détectable ?" } }, "ja": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "このMacを検出可能にしますか?" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "이 Mac을 검색 가능하게 할까요?" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "允许发现此 Mac 吗?" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "要允許探索此 Mac 嗎?" } }, "bs": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Učiniti ovaj Mac dostupnim?" } }, "da": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Gør denne Mac synlig?" } }, "it": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Rendere individuabile questo Mac?" } }, "km": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "ធ្វើឲ្យ Mac នេះអាចរកឃើញ?" } }, "nb": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Gjøre denne Mac-en synlig?" } }, "pl": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Uwidocznić tego Maca?" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Tornar este Mac detectável?" } }, "ru": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Сделать этот Mac доступным для обнаружения?" } }, "th": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "ทำให้ Mac เครื่องนี้ค้นพบได้หรือไม่" } }, "tr": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Bu Mac keşfedilebilir yapılsın mı?" } }, "uk": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Зробити цей Mac доступним для виявлення?" + } + } + } + }, + "cloudTree.device.status.connecting": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ الاتصال…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird verbunden…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conectando…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "接続中…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "연결 중…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在连接…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在連線…" } } } @@ -582968,603 +583322,1772 @@ } } }, - "cloudTree.action.newCloudMachine": { + "shortcut.diffViewerNextHunk.label": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "New Cloud Machine" + "value": "Diff Viewer: Next Hunk" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "差分ビューア: 次のハンク" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "差异查看器:下一个块" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diff-Viewer: Nächster Hunk" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Visionneuse de diff : Bloc suivant" } }, "ar": { "stringUnit": { "state": "translated", - "value": "جهاز سحابي جديد" + "value": "عارض الفروق: المقطع التالي" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Visor de diff: Siguiente bloque" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "差異檢視器:下一個區塊" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "비교 뷰어: 다음 헝크" + } + } + } + }, + "shortcut.diffViewerPreviousHunk.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Diff Viewer: Previous Hunk" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "差分ビューア: 前のハンク" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "差异查看器:上一个块" } }, "de": { "stringUnit": { "state": "translated", - "value": "Neue Cloud-Maschine" + "value": "Diff-Viewer: Vorheriger Hunk" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Visionneuse de diff : Bloc précédent" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "عارض الفروق: المقطع السابق" } }, "es": { "stringUnit": { "state": "translated", - "value": "Nueva máquina en la nube" + "value": "Visor de diff: Bloque anterior" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "差異檢視器:上一個區塊" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "비교 뷰어: 이전 헝크" + } + } + } + }, + "cloudMenu.machine.freeAccess": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Free access" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "وصول مجاني" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kostenloser Zugang" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Acceso gratuito" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Nouvelle machine cloud" + "value": "Accès gratuit" } }, "ja": { "stringUnit": { "state": "translated", - "value": "新しいクラウドマシン" + "value": "無料アクセス" } }, "ko": { "stringUnit": { "state": "translated", - "value": "새 클라우드 머신" + "value": "무료 이용" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "新建云主机" + "value": "免费访问" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "新增雲端機器" + "value": "免費存取" + } + } + } + }, + "cloudMenu.machine.locked": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Locked" } }, - "bs": { + "ar": { "stringUnit": { "state": "translated", - "value": "Nova cloud mašina" + "value": "مقفلة" } }, - "da": { + "de": { "stringUnit": { "state": "translated", - "value": "Ny cloud-maskine" + "value": "Gesperrt" } }, - "it": { + "es": { "stringUnit": { "state": "translated", - "value": "Nuova macchina cloud" + "value": "Bloqueada" } }, - "km": { + "fr": { "stringUnit": { "state": "translated", - "value": "ម៉ាស៊ីន Cloud ថ្មី" + "value": "Verrouillée" } }, - "nb": { + "ja": { "stringUnit": { "state": "translated", - "value": "Ny sky-maskin" + "value": "ロック中" } }, - "pl": { + "ko": { "stringUnit": { "state": "translated", - "value": "Nowa maszyna w chmurze" + "value": "잠김" } }, - "pt-BR": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Nova máquina na nuvem" + "value": "已锁定" } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Новая облачная машина" + "value": "已鎖定" + } + } + } + }, + "cloudMenu.dashboard": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Cloud Dashboard" } }, - "th": { + "ar": { "stringUnit": { "state": "translated", - "value": "เครื่องคลาวด์ใหม่" + "value": "فتح لوحة تحكم السحابة" } }, - "tr": { + "de": { "stringUnit": { "state": "translated", - "value": "Yeni bulut makinesi" + "value": "Cloud-Dashboard öffnen" } }, - "uk": { + "es": { "stringUnit": { "state": "translated", - "value": "Нова хмарна машина" + "value": "Abrir el panel de Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le tableau de bord Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドダッシュボードを開く" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 대시보드 열기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "打开云端控制面板" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "打開雲端控制面板" } } } }, - "update.readyWaiting.agents": { + "cloudMenu.diagnostics": { "extractionState": "manual", "localizations": { "en": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "Installs and relaunches when %lld agent finishes. Agents resume after the relaunch." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "Installs and relaunches when %lld agents finish. Agents resume after the relaunch." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Cloud Diagnostics" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تشخيصات السحابة" } }, "de": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "Wird installiert und neu gestartet, sobald %lld Agent fertig ist. Agenten werden nach dem Neustart fortgesetzt." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "Wird installiert und neu gestartet, sobald %lld Agenten fertig sind. Agenten werden nach dem Neustart fortgesetzt." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Cloud-Diagnose" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Diagnóstico de Cloud" } }, "fr": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "S’installe et redémarre quand %lld agent a terminé. Les agents reprennent après le redémarrage." - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "S’installe et redémarre quand %lld d’agents ont terminé. Les agents reprennent après le redémarrage." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "S’installe et redémarre quand %lld agents ont terminé. Les agents reprennent après le redémarrage." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Diagnostics Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウド診断" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 진단" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端诊断" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端診斷" + } + } + } + }, + "cloudMenu.machines.empty": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No machines yet" } }, "ar": { - "variations": { - "plural": { - "zero": { - "stringUnit": { - "state": "translated", - "value": "يتم التثبيت وإعادة التشغيل عند انتهاء الوكلاء (%lld). تستأنف الوكلاء بعد إعادة التشغيل." - } - }, - "one": { - "stringUnit": { - "state": "translated", - "value": "يتم التثبيت وإعادة التشغيل عند انتهاء وكيل واحد (%lld). تستأنف الوكلاء بعد إعادة التشغيل." - } - }, - "two": { - "stringUnit": { - "state": "translated", - "value": "يتم التثبيت وإعادة التشغيل عند انتهاء وكيلين (%lld). تستأنف الوكلاء بعد إعادة التشغيل." - } - }, - "few": { - "stringUnit": { - "state": "translated", - "value": "يتم التثبيت وإعادة التشغيل عند انتهاء %lld وكلاء. تستأنف الوكلاء بعد إعادة التشغيل." - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "يتم التثبيت وإعادة التشغيل عند انتهاء %lld وكيلًا. تستأنف الوكلاء بعد إعادة التشغيل." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "يتم التثبيت وإعادة التشغيل عند انتهاء %lld وكيل. تستأنف الوكلاء بعد إعادة التشغيل." - } - } - } + "stringUnit": { + "state": "translated", + "value": "لا توجد أجهزة بعد" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Maschinen" } }, "es": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "Se instala y reinicia cuando termine %lld agente. Los agentes se reanudan tras el reinicio." - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "Se instala y reinicia cuando terminen %lld de agentes. Los agentes se reanudan tras el reinicio." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "Se instala y reinicia cuando terminen %lld agentes. Los agentes se reanudan tras el reinicio." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Aún no hay máquinas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune machine pour le moment" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "マシンはまだありません" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "아직 머신이 없습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "还没有机器" } }, "zh-Hant": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "將在 %lld 個代理完成後安裝並重新啟動。代理會在重新啟動後繼續。" - } - } - } + "stringUnit": { + "state": "translated", + "value": "尚無機器" + } + } + } + }, + "cloudMenu.machines.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أجهزة السحابة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Maschinen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Máquinas de Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Machines Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドマシン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 머신" } }, "zh-Hans": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "将在 %lld 个代理完成后安装并重新启动。代理会在重新启动后继续。" - } - } - } + "stringUnit": { + "state": "translated", + "value": "云端机器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端機器" + } + } + } + }, + "cloudMenu.more": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "More Cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "المزيد من السحابة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mehr Cloud" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Más de Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plus de Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "その他のクラウド" } }, "ko": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "에이전트 %lld개가 끝나면 설치하고 다시 시작합니다. 다시 시작한 후 에이전트가 재개됩니다." - } - } - } + "stringUnit": { + "state": "translated", + "value": "클라우드 더 보기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "更多云端项目" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "更多雲端項目" + } + } + } + }, + "cloudMenu.problem.requiresPro": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud machines need cmux Pro" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تتطلب أجهزة السحابة cmux Pro" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Maschinen erfordern cmux Pro" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las máquinas de Cloud requieren cmux Pro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les machines Cloud nécessitent cmux Pro" } }, "ja": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%lld 個のエージェントが終了するとインストールして再起動します。エージェントは再起動後に再開されます。" - } - } - } + "stringUnit": { + "state": "translated", + "value": "クラウドマシンには cmux Pro が必要です" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 머신에는 cmux Pro가 필요합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端机器需要 cmux Pro" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端機器需要 cmux Pro" } } } }, - "update.readyWaiting.commands": { + "cloudMenu.problem.session": { "extractionState": "manual", "localizations": { "en": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "Relaunching will stop %lld running command. Choose Install Now when you're ready." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "Relaunching will stop %lld running commands. Choose Install Now when you're ready." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Your Cloud session expired" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "انتهت صلاحية جلسة السحابة" } }, "de": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "Der Neustart beendet %lld laufenden Befehl. Wähle „Jetzt installieren“, wenn du bereit bist." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "Der Neustart beendet %lld laufende Befehle. Wähle „Jetzt installieren“, wenn du bereit bist." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Deine Cloud-Sitzung ist abgelaufen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu sesión de Cloud ha caducado" } }, "fr": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "Le redémarrage arrêtera %lld commande en cours. Choisissez Installer maintenant quand vous êtes prêt." - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "Le redémarrage arrêtera %lld de commandes en cours. Choisissez Installer maintenant quand vous êtes prêt." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "Le redémarrage arrêtera %lld commandes en cours. Choisissez Installer maintenant quand vous êtes prêt." - } - } - } + "stringUnit": { + "state": "translated", + "value": "Votre session Cloud a expiré" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドのセッションが期限切れです" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 세션이 만료되었습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "你的云端会话已过期" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "你的雲端工作階段已過期" + } + } + } + }, + "cloudMenu.problem.unreachable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not reach cmux Cloud" } }, "ar": { - "variations": { - "plural": { - "zero": { - "stringUnit": { - "state": "translated", - "value": "ستؤدي إعادة التشغيل إلى إيقاف الأوامر قيد التشغيل (%lld). اختر التثبيت الآن عندما تكون جاهزًا." - } - }, - "one": { - "stringUnit": { - "state": "translated", - "value": "ستؤدي إعادة التشغيل إلى إيقاف أمر واحد قيد التشغيل (%lld). اختر التثبيت الآن عندما تكون جاهزًا." - } - }, - "two": { - "stringUnit": { - "state": "translated", - "value": "ستؤدي إعادة التشغيل إلى إيقاف أمرين قيد التشغيل (%lld). اختر التثبيت الآن عندما تكون جاهزًا." - } - }, - "few": { - "stringUnit": { - "state": "translated", - "value": "ستؤدي إعادة التشغيل إلى إيقاف %lld أوامر قيد التشغيل. اختر التثبيت الآن عندما تكون جاهزًا." - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "ستؤدي إعادة التشغيل إلى إيقاف %lld أمرًا قيد التشغيل. اختر التثبيت الآن عندما تكون جاهزًا." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "ستؤدي إعادة التشغيل إلى إيقاف %lld أمر قيد التشغيل. اختر التثبيت الآن عندما تكون جاهزًا." - } - } - } + "stringUnit": { + "state": "translated", + "value": "تعذر الوصول إلى cmux Cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux Cloud ist nicht erreichbar" } }, "es": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "El reinicio detendrá %lld comando en ejecución. Elige Instalar ahora cuando estés listo." - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "El reinicio detendrá %lld de comandos en ejecución. Elige Instalar ahora cuando estés listo." - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "El reinicio detendrá %lld comandos en ejecución. Elige Instalar ahora cuando estés listo." - } - } - } + "stringUnit": { + "state": "translated", + "value": "No se pudo conectar con cmux Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de joindre cmux Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux Cloud に接続できません" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux Cloud에 연결할 수 없습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法连接 cmux Cloud" } }, "zh-Hant": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "重新啟動將停止 %lld 個執行中的命令。準備好後請選擇「立即安裝」。" - } - } - } + "stringUnit": { + "state": "translated", + "value": "無法連線到 cmux Cloud" + } + } + } + }, + "cloudMenu.retry": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Try Again" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة المحاولة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再試行" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다시 시도" } }, "zh-Hans": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "重新启动将停止 %lld 个正在运行的命令。准备好后请选择“立即安装”。" - } - } - } + "stringUnit": { + "state": "translated", + "value": "重试" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "再試一次" + } + } + } + }, + "cloudMenu.showMachines": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Machines Sidebar" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار الشريط الجانبي للأجهزة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Maschinen-Seitenleiste anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar la barra lateral de máquinas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher la barre latérale des machines" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "マシンのサイドバーを表示" } }, "ko": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "다시 시작하면 실행 중인 명령 %lld개가 중지됩니다. 준비되면 지금 설치를 선택하세요." - } - } - } + "stringUnit": { + "state": "translated", + "value": "머신 사이드바 보기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示机器侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示機器側邊欄" + } + } + } + }, + "cloudMenu.signOut": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign Out" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تسجيل الخروج" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abmelden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cerrar sesión" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se déconnecter" } }, "ja": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "再起動すると実行中の %lld 個のコマンドが停止します。準備ができたら「今すぐインストール」を選択してください。" - } - } - } + "stringUnit": { + "state": "translated", + "value": "サインアウト" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "로그아웃" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "退出登录" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "登出" } } } }, - "shortcut.diffViewerNextHunk.label": { + "cloudMenu.signedIn": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Diff Viewer: Next Hunk" + "value": "Signed In" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تم تسجيل الدخول" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Angemeldet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sesión iniciada" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connecté" } }, "ja": { "stringUnit": { "state": "translated", - "value": "差分ビューア: 次のハンク" + "value": "サインイン済み" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "로그인됨" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "差异查看器:下一个块" + "value": "已登录" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已登入" + } + } + } + }, + "cloudMenu.team": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Team" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الفريق" } }, "de": { "stringUnit": { "state": "translated", - "value": "Diff-Viewer: Nächster Hunk" + "value": "Team" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Equipo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Visionneuse de diff : Bloc suivant" + "value": "Équipe" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "チーム" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "팀" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "团队" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "團隊" + } + } + } + }, + "cloudMenu.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud" } }, "ar": { "stringUnit": { "state": "translated", - "value": "عارض الفروق: المقطع التالي" + "value": "السحابة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud" } }, "es": { "stringUnit": { "state": "translated", - "value": "Visor de diff: Siguiente bloque" + "value": "Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウド" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "差異檢視器:下一個區塊" + "value": "雲端" + } + } + } + }, + "cloudMenu.tone.attention": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Needs attention" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يحتاج إلى انتباه" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erfordert Aufmerksamkeit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Requiere atención" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite une attention" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "対応が必要" } }, "ko": { "stringUnit": { "state": "translated", - "value": "비교 뷰어: 다음 헝크" + "value": "확인 필요" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "需要处理" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "需要處理" } } } }, - "shortcut.diffViewerPreviousHunk.label": { + "menu.cloud.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Diff Viewer: Previous Hunk" + "value": "Cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "السحابة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cloud" } }, "ja": { "stringUnit": { "state": "translated", - "value": "差分ビューア: 前のハンク" + "value": "クラウド" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "差异查看器:上一个块" + "value": "云端" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端" + } + } + } + }, + "cloudMenu.machines.loading": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines · Loading…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أجهزة السحابة · جارٍ التحميل…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Diff-Viewer: Vorheriger Hunk" + "value": "Cloud-Maschinen · Wird geladen…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Máquinas de Cloud · Cargando…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Visionneuse de diff : Bloc précédent" + "value": "Machines Cloud · Chargement…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドマシン · 読み込み中…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 머신 · 불러오는 중…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端机器 · 正在加载…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端機器 · 正在載入…" + } + } + } + }, + "cloudMenu.machines.showAll": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show All Machines…" } }, "ar": { "stringUnit": { "state": "translated", - "value": "عارض الفروق: المقطع السابق" + "value": "عرض كل الأجهزة…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Maschinen anzeigen…" } }, "es": { "stringUnit": { "state": "translated", - "value": "Visor de diff: Bloque anterior" + "value": "Mostrar todas las máquinas…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher toutes les machines…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "すべてのマシンを表示…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "모든 머신 보기…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示所有机器…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "差異檢視器:上一個區塊" + "value": "顯示所有機器…" + } + } + } + }, + "cloudMenu.newMachine": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New Cloud Machine…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جهاز سحابي جديد…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Cloud-Maschine…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nueva máquina de Cloud…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouvelle machine Cloud…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規クラウドマシン…" } }, "ko": { "stringUnit": { "state": "translated", - "value": "비교 뷰어: 이전 헝크" + "value": "새 클라우드 머신…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建云端机器…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增雲端機器…" + } + } + } + }, + "cloudMenu.signIn": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign In to cmux Cloud…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تسجيل الدخول إلى cmux Cloud…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bei cmux Cloud anmelden…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iniciar sesión en cmux Cloud…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se connecter à cmux Cloud…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux Cloud にサインイン…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux Cloud에 로그인…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "登录 cmux Cloud…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "登入 cmux Cloud…" + } + } + } + }, + "cloudMenu.signInAgain": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sign In Again…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تسجيل الدخول مجددًا…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut anmelden…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver a iniciar sesión…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se reconnecter…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "もう一度サインイン…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다시 로그인…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重新登录…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新登入…" + } + } + } + }, + "cloudMenu.signingIn": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Signing In…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ تسجيل الدخول…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anmeldung läuft…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iniciando sesión…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サインイン中…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "로그인 중…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在登录…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在登入…" + } + } + } + }, + "cloudMenu.upgrade": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Upgrade to Pro…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الترقية إلى Pro…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf Pro upgraden…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mejorar a Pro…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passer à Pro…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Pro にアップグレード…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Pro로 업그레이드…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "升级到 Pro…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "升級至 Pro…" + } + } + } + }, + "settings.search.alias.section.themes": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent design designs farbschema farben palette hell dunkel akzent" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent thème thèmes couleurs palette clair sombre accent" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent سمة سمات ألوان لوحة فاتح داكن تمييز" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent tema temas colores paleta claro oscuro acento" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent 主題 配色 顏色 調色盤 淺色 深色 強調色" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent 主题 配色 颜色 调色板 浅色 深色 强调色" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent 테마 색상 팔레트 라이트 다크 강조색" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "theme themes color scheme colors palette ghostty light dark mode accent テーマ 配色 カラー パレット ライト ダーク アクセント" + } + } + } + }, + "settings.section.themes": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Themes" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "السمات" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Designs" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Temas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thèmes" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テーマ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "테마" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "主题" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "主題" + } + } + } + }, + "settings.terminal.theme": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Terminal Theme" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "سمة الطرفية" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Terminal-Design" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tema del terminal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thème du terminal" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ターミナルテーマ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "터미널 테마" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "终端主题" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "終端機主題" } } } @@ -586294,2539 +587817,3238 @@ } } }, - "settings.search.alias.section.themes": { + "cloudTree.action.newCloudMachine": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent" + "value": "New Cloud Machine" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جهاز سحابي جديد" } }, "de": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent design designs farbschema farben palette hell dunkel akzent" + "value": "Neue Cloud-Maschine" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent thème thèmes couleurs palette clair sombre accent" + "value": "Nueva máquina en la nube" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent سمة سمات ألوان لوحة فاتح داكن تمييز" + "value": "Nouvelle machine cloud" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent tema temas colores paleta claro oscuro acento" + "value": "新しいクラウドマシン" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent 主題 配色 顏色 調色盤 淺色 深色 強調色" + "value": "새 클라우드 머신" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent 主题 配色 颜色 调色板 浅色 深色 强调色" + "value": "新建云主机" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent 테마 색상 팔레트 라이트 다크 강조색" + "value": "新增雲端機器" } }, - "ja": { + "bs": { "stringUnit": { "state": "translated", - "value": "theme themes color scheme colors palette ghostty light dark mode accent テーマ 配色 カラー パレット ライト ダーク アクセント" + "value": "Nova cloud mašina" } - } - } - }, - "settings.section.themes": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Themes" + "value": "Ny cloud-maskine" } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "السمات" + "value": "Nuova macchina cloud" } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Designs" + "value": "ម៉ាស៊ីន Cloud ថ្មី" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Temas" + "value": "Ny sky-maskin" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Thèmes" + "value": "Nowa maszyna w chmurze" } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "テーマ" + "value": "Nova máquina na nuvem" } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "테마" + "value": "Новая облачная машина" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "主题" + "value": "เครื่องคลาวด์ใหม่" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "主題" + "value": "Yeni bulut makinesi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Нова хмарна машина" } } } }, - "settings.terminal.theme": { + "terminalContextMenu.copyLink": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Terminal Theme" + "value": "Copy Link" } }, "ar": { "stringUnit": { "state": "translated", - "value": "سمة الطرفية" + "value": "نسخ الرابط" } }, "de": { "stringUnit": { "state": "translated", - "value": "Terminal-Design" + "value": "Link kopieren" } }, "es": { "stringUnit": { "state": "translated", - "value": "Tema del terminal" + "value": "Copiar enlace" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Thème du terminal" + "value": "Copier le lien" } }, "ja": { "stringUnit": { "state": "translated", - "value": "ターミナルテーマ" + "value": "リンクをコピー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "터미널 테마" + "value": "링크 복사" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "终端主题" + "value": "复制链接" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "終端機主題" + "value": "複製連結" } } } }, - "cloudMenu.machine.freeAccess": { + "terminalContextMenu.openLinkInCmuxBrowser": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Free access" + "value": "Open Link in cmux Browser" } }, "ar": { "stringUnit": { "state": "translated", - "value": "وصول مجاني" + "value": "فتح الرابط في متصفح cmux" } }, "de": { "stringUnit": { "state": "translated", - "value": "Kostenloser Zugang" + "value": "Link im cmux-Browser öffnen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Acceso gratuito" + "value": "Abrir enlace en el navegador de cmux" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Accès gratuit" + "value": "Ouvrir le lien dans le navigateur cmux" } }, "ja": { "stringUnit": { "state": "translated", - "value": "無料アクセス" + "value": "リンクをcmuxブラウザで開く" } }, "ko": { "stringUnit": { "state": "translated", - "value": "무료 이용" + "value": "링크를 cmux 브라우저에서 열기" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "免费访问" + "value": "在 cmux 浏览器中打开链接" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "免費存取" + "value": "在 cmux 瀏覽器中開啟連結" } } } }, - "cloudMenu.machine.locked": { + "terminalContextMenu.openLinkInDefaultBrowser": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Locked" + "value": "Open Link in Default Browser" } }, "ar": { "stringUnit": { "state": "translated", - "value": "مقفلة" + "value": "فتح الرابط في المتصفح الافتراضي" } }, "de": { "stringUnit": { "state": "translated", - "value": "Gesperrt" + "value": "Link im Standardbrowser öffnen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Bloqueada" + "value": "Abrir enlace en el navegador predeterminado" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Verrouillée" + "value": "Ouvrir le lien dans le navigateur par défaut" } }, "ja": { "stringUnit": { "state": "translated", - "value": "ロック中" + "value": "リンクをデフォルトブラウザで開く" } }, "ko": { "stringUnit": { "state": "translated", - "value": "잠김" + "value": "링크를 기본 브라우저에서 열기" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已锁定" + "value": "在默认浏览器中打开链接" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已鎖定" + "value": "在預設瀏覽器中開啟連結" } } } }, - "cloudMenu.dashboard": { + "cli.restore.error.routedLauncherNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Open Cloud Dashboard" + "value": "restore: this session was started with '%1$@ claude proxy', but '%1$@' is not on PATH in this shell. Add it to PATH, then retry." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "فتح لوحة تحكم السحابة" + "value": "restore: このセッションは「%1$@ claude proxy」で起動されましたが、このシェルの PATH に「%1$@」がありません。PATH に追加してから、再試行してください。" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud-Dashboard öffnen" + "value": "restore:此会话是通过“%1$@ claude proxy”启动的,但此 shell 的 PATH 中没有“%1$@”。请将其添加到 PATH,然后重试。" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Abrir el panel de Cloud" + "value": "restore: Diese Sitzung wurde mit „%1$@ claude proxy“ gestartet, aber „%1$@“ ist in dieser Shell nicht im PATH. Fügen Sie es zum PATH hinzu und versuchen Sie es dann erneut." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ouvrir le tableau de bord Cloud" + "value": "restore : cette session a été démarrée avec « %1$@ claude proxy », mais « %1$@ » n'est pas dans le PATH de ce shell. Ajoutez-le au PATH, puis réessayez." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "クラウドダッシュボードを開く" + "value": "restore: بدأت هذه الجلسة باستخدام '%1$@ claude proxy'، لكن '%1$@' غير موجود في PATH في هذه الصدفة. أضفه إلى PATH، ثم أعد المحاولة." } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "클라우드 대시보드 열기" + "value": "restore: esta sesión se inició con '%1$@ claude proxy', pero '%1$@' no está en el PATH de este shell. Agréguelo al PATH y vuelva a intentarlo." } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "打开云端控制面板" + "value": "restore:此工作階段是以「%1$@ claude proxy」啟動的,但此 shell 的 PATH 中沒有「%1$@」。請將其加入 PATH,然後重試。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "打開雲端控制面板" + "value": "restore: 이 세션은 '%1$@ claude proxy'로 시작되었지만 이 셸의 PATH에 '%1$@'이(가) 없습니다. PATH에 추가한 후 다시 시도하십시오." } } } }, - "cloudMenu.diagnostics": { + "settings.terminal.textEditingCommandMovesByWord": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud Diagnostics" + "value": "Command Moves by Word" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تشخيصات السحابة" + "value": "Command ينقل حسب الكلمة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Diagnose" + "value": "Befehlstaste bewegt wortweise" } }, "es": { "stringUnit": { "state": "translated", - "value": "Diagnóstico de Cloud" + "value": "Comando mueve por palabra" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Diagnostics Cloud" + "value": "Commande déplace par mot" } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウド診断" + "value": "Command で単語単位に移動" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드 진단" + "value": "Command로 단어 단위 이동" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端诊断" + "value": "Command 按词移动" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "雲端診斷" + "value": "Command 依字移動" } } } }, - "cloudMenu.machines.empty": { + "settings.terminal.textEditingCommandMovesByWord.subtitleOn": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No machines yet" + "value": "Command arrow and delete keys move and delete by word, like Option. Control Left and Right Arrow move to the start and end of the line." } }, "ar": { "stringUnit": { "state": "translated", - "value": "لا توجد أجهزة بعد" + "value": "تنقل مفاتيح الأسهم والحذف مع Command وتحذف حسب الكلمة، مثل Option. وينقل Control مع السهمين الأيسر والأيمن إلى بداية السطر ونهايته." } }, "de": { "stringUnit": { "state": "translated", - "value": "Noch keine Maschinen" + "value": "Befehlstaste mit Pfeil- und Löschtasten bewegt und löscht wortweise, wie die Wahltaste. Control mit Links- und Rechtspfeil springt an Anfang und Ende der Zeile." } }, "es": { "stringUnit": { "state": "translated", - "value": "Aún no hay máquinas" + "value": "Las flechas y la tecla Eliminar con Comando mueven y borran por palabra, como Opción. Control con las flechas izquierda y derecha va al inicio y al final de la línea." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Aucune machine pour le moment" + "value": "Les flèches et la touche Supprimer avec Commande déplacent et suppriment par mot, comme Option. Contrôle avec les flèches gauche et droite va au début et à la fin de la ligne." } }, "ja": { "stringUnit": { "state": "translated", - "value": "マシンはまだありません" + "value": "Command と矢印キー・削除キーで、Option と同じく単語単位に移動・削除します。Control と左右矢印キーで行頭・行末に移動します。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "아직 머신이 없습니다" + "value": "Command 화살표 및 삭제 키가 Option처럼 단어 단위로 이동하고 삭제합니다. Control 왼쪽 및 오른쪽 화살표는 줄의 시작과 끝으로 이동합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "还没有机器" + "value": "Command 加方向键和删除键按词移动和删除,与 Option 相同。Control 加左右方向键移到行首和行尾。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尚無機器" + "value": "Command 加方向鍵與刪除鍵會依字移動與刪除,與 Option 相同。Control 加左右方向鍵會移到行首與行尾。" } } } }, - "cloudMenu.machines.title": { + "settings.terminal.textEditingCommandMovesByWord.subtitleOff": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud Machines" + "value": "Command arrow and delete keys move and delete by line, as in macOS text fields." } }, "ar": { "stringUnit": { "state": "translated", - "value": "أجهزة السحابة" + "value": "تنقل مفاتيح الأسهم والحذف مع Command وتحذف حسب السطر، كما في حقول النص في macOS." } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen" + "value": "Befehlstaste mit Pfeil- und Löschtasten bewegt und löscht zeilenweise, wie in macOS-Textfeldern." } }, "es": { "stringUnit": { "state": "translated", - "value": "Máquinas de Cloud" + "value": "Las flechas y la tecla Eliminar con Comando mueven y borran por línea, como en los campos de texto de macOS." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Machines Cloud" + "value": "Les flèches et la touche Supprimer avec Commande déplacent et suppriment par ligne, comme dans les champs de texte de macOS." } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウドマシン" + "value": "Command と矢印キー・削除キーで、macOS のテキストフィールドと同じく行単位に移動・削除します。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드 머신" + "value": "Command 화살표 및 삭제 키가 macOS 텍스트 필드처럼 줄 단위로 이동하고 삭제합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端机器" + "value": "Command 加方向键和删除键按行移动和删除,与 macOS 文本框相同。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "雲端機器" + "value": "Command 加方向鍵與刪除鍵會依行移動與刪除,與 macOS 文字欄位相同。" } } } }, - "cloudMenu.more": { + "settings.terminal.textEditingGesturesInFullScreenApps": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "More Cloud" + "value": "Text Editing Gestures in Full-Screen Apps" } }, "ar": { "stringUnit": { "state": "translated", - "value": "المزيد من السحابة" + "value": "إيماءات تحرير النص في التطبيقات بملء الشاشة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Mehr Cloud" + "value": "Textbearbeitungsgesten in Vollbild-Apps" } }, "es": { "stringUnit": { "state": "translated", - "value": "Más de Cloud" + "value": "Gestos de edición de texto en apps a pantalla completa" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Plus de Cloud" + "value": "Gestes d’édition de texte dans les apps plein écran" } }, "ja": { "stringUnit": { "state": "translated", - "value": "その他のクラウド" + "value": "フルスクリーンアプリでのテキスト編集ジェスチャー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드 더 보기" + "value": "전체 화면 앱에서 텍스트 편집 제스처" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "更多云端项目" + "value": "全屏应用中的文本编辑手势" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "更多雲端項目" + "value": "全螢幕 App 中的文字編輯手勢" } } } }, - "cloudMenu.problem.requiresPro": { + "settings.terminal.textEditingGesturesInFullScreenApps.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud machines need cmux Pro" + "value": "Gestures also apply while a full-screen app such as vim, less, or tmux is running, so they keep working at a shell inside tmux, screen, or zellij." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تتطلب أجهزة السحابة cmux Pro" + "value": "تنطبق الإيماءات أيضًا أثناء تشغيل تطبيق بملء الشاشة مثل vim أو less أو tmux، فتظل تعمل في الصدفة داخل tmux أو screen أو zellij." } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen erfordern cmux Pro" + "value": "Gesten gelten auch, während eine Vollbild-App wie vim, less oder tmux läuft, und funktionieren so weiter an einer Shell in tmux, screen oder zellij." } }, "es": { "stringUnit": { "state": "translated", - "value": "Las máquinas de Cloud requieren cmux Pro" + "value": "Los gestos también se aplican mientras se ejecuta una app a pantalla completa como vim, less o tmux, así que siguen funcionando en una shell dentro de tmux, screen o zellij." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les machines Cloud nécessitent cmux Pro" + "value": "Les gestes s’appliquent aussi lorsqu’une app plein écran comme vim, less ou tmux est en cours d’exécution, et continuent donc de fonctionner dans un shell sous tmux, screen ou zellij." } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウドマシンには cmux Pro が必要です" + "value": "vim、less、tmux などのフルスクリーンアプリの実行中もジェスチャーが有効になるため、tmux、screen、zellij 内のシェルでも引き続き使えます。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드 머신에는 cmux Pro가 필요합니다" + "value": "vim, less, tmux 같은 전체 화면 앱이 실행 중일 때도 제스처가 적용되므로 tmux, screen, zellij 안의 셸에서도 계속 작동합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端机器需要 cmux Pro" + "value": "在 vim、less 或 tmux 等全屏应用运行时手势也会生效,因此在 tmux、screen 或 zellij 中的 shell 里仍可使用。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "雲端機器需要 cmux Pro" + "value": "在 vim、less 或 tmux 等全螢幕 App 執行時手勢也會生效,因此在 tmux、screen 或 zellij 中的 shell 裡仍可使用。" } } } }, - "cloudMenu.problem.session": { + "actions.discovery.typeDetail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Your Cloud session expired" + "value": "Action type: %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "انتهت صلاحية جلسة السحابة" + "value": "نوع الإجراء: %@" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Vrsta radnje: %@" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Handlingstype: %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Deine Cloud-Sitzung ist abgelaufen" + "value": "Aktionstyp: %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Tu sesión de Cloud ha caducado" + "value": "Tipo de acción: %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Votre session Cloud a expiré" + "value": "Type d’action : %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tipo di azione: %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウドのセッションが期限切れです" + "value": "アクションの種類:%@" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្រភេទសកម្មភាព៖ %@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드 세션이 만료되었습니다" + "value": "작업 유형: %@" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Handlingstype: %@" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Typ działania: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tipo de ação: %@" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Тип действия: %@" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ประเภทการดำเนินการ: %@" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Eylem türü: %@" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Тип дії: %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "你的云端会话已过期" + "value": "操作类型:%@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "你的雲端工作階段已過期" + "value": "動作類型:%@" } } } }, - "cloudMenu.problem.unreachable": { + "actions.discovery.shortcutDetail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Could not reach cmux Cloud" + "value": "Shortcut: %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تعذر الوصول إلى cmux Cloud" + "value": "اختصار لوحة المفاتيح: %@" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prečica: %@" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Genvej: %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux Cloud ist nicht erreichbar" + "value": "Tastenkürzel: %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo conectar con cmux Cloud" + "value": "Atajo: %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Impossible de joindre cmux Cloud" + "value": "Raccourci : %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scorciatoia: %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux Cloud に接続できません" + "value": "ショートカット:%@" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្លូវកាត់៖ %@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux Cloud에 연결할 수 없습니다" + "value": "단축키: %@" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Snarvei: %@" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Skrót: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atalho: %@" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Сочетание клавиш: %@" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แป้นพิมพ์ลัด: %@" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Klavye kısayolu: %@" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Сполучення клавіш: %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无法连接 cmux Cloud" + "value": "快捷键:%@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法連線到 cmux Cloud" + "value": "快速鍵:%@" } } } }, - "cloudMenu.retry": { + "actions.discovery.menuTitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Try Again" + "value": "Actions · cmux.json…" } }, "ar": { "stringUnit": { "state": "translated", - "value": "إعادة المحاولة" + "value": "الإجراءات · cmux.json…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Erneut versuchen" + "value": "Aktionen · cmux.json…" } }, "es": { "stringUnit": { "state": "translated", - "value": "Reintentar" + "value": "Acciones · cmux.json…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Réessayer" + "value": "Actions · cmux.json…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "再試行" + "value": "アクション · cmux.json…" } }, "ko": { "stringUnit": { "state": "translated", - "value": "다시 시도" + "value": "작업 · cmux.json…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "重试" + "value": "操作 · cmux.json…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "再試一次" + "value": "動作 · cmux.json…" } } } }, - "cloudMenu.showMachines": { + "actions.discovery.dialogTitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Show Machines Sidebar" + "value": "Actions · cmux.json" } }, "ar": { "stringUnit": { "state": "translated", - "value": "إظهار الشريط الجانبي للأجهزة" + "value": "الإجراءات · cmux.json" } }, "de": { "stringUnit": { "state": "translated", - "value": "Maschinen-Seitenleiste anzeigen" + "value": "Aktionen · cmux.json" } }, "es": { "stringUnit": { "state": "translated", - "value": "Mostrar la barra lateral de máquinas" + "value": "Acciones · cmux.json" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Afficher la barre latérale des machines" + "value": "Actions · cmux.json" } }, "ja": { "stringUnit": { "state": "translated", - "value": "マシンのサイドバーを表示" + "value": "アクション · cmux.json" } }, "ko": { "stringUnit": { "state": "translated", - "value": "머신 사이드바 보기" + "value": "작업 · cmux.json" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "显示机器侧边栏" + "value": "操作 · cmux.json" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "顯示機器側邊欄" + "value": "動作 · cmux.json" } } } }, - "cloudMenu.signOut": { + "actions.discovery.empty": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sign Out" + "value": "No configured actions." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تسجيل الخروج" + "value": "لا توجد إجراءات مُعدّة." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Nema konfigurisanih radnji." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ingen konfigurerede handlinger." } }, "de": { "stringUnit": { "state": "translated", - "value": "Abmelden" + "value": "Keine konfigurierten Aktionen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Cerrar sesión" + "value": "No hay acciones configuradas." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Se déconnecter" + "value": "Aucune action configurée." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna azione configurata." } }, "ja": { "stringUnit": { "state": "translated", - "value": "サインアウト" + "value": "設定済みのアクションはありません。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "មិនមានសកម្មភាពដែលបានកំណត់រចនាសម្ព័ន្ធទេ។" } }, "ko": { "stringUnit": { "state": "translated", - "value": "로그아웃" + "value": "구성된 작업이 없습니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Ingen konfigurerte handlinger." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Brak skonfigurowanych działań." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma ação configurada." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Нет настроенных действий." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่มีการดำเนินการที่กำหนดค่าไว้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yapılandırılmış eylem yok." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Немає налаштованих дій." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "退出登录" + "value": "没有已配置的操作。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "登出" + "value": "沒有已設定的動作。" } } } }, - "cloudMenu.signedIn": { + "agentWake.banner.close": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Signed In" + "value": "Close" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "تم تسجيل الدخول" + "value": "閉じる" } }, "de": { "stringUnit": { "state": "translated", - "value": "Angemeldet" + "value": "Schließen" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Sesión iniciada" + "value": "Fermer" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Connecté" + "value": "إغلاق" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "サインイン済み" + "value": "Cerrar" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "로그인됨" + "value": "關閉" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已登录" + "value": "关闭" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "已登入" + "value": "닫기" } } } }, - "cloudMenu.team": { + "agentWake.banner.copy": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Team" + "value": "Copy" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "الفريق" + "value": "コピー" } }, "de": { "stringUnit": { "state": "translated", - "value": "Team" + "value": "Kopieren" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Equipo" + "value": "Copier" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Équipe" + "value": "نسخ" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "チーム" + "value": "Copiar" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "팀" + "value": "複製" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "团队" + "value": "复制" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "團隊" + "value": "복사" } } } }, - "cloudMenu.title": { + "agentWake.banner.hideCommand": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "Hide command" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "السحابة" + "value": "コマンドを隠す" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "Befehl ausblenden" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "Masquer la commande" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "إخفاء الأمر" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "クラウド" + "value": "Ocultar comando" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "클라우드" + "value": "隱藏指令" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端" + "value": "隐藏命令" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "雲端" + "value": "명령 숨기기" } } } }, - "cloudMenu.tone.attention": { + "agentWake.banner.message": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Needs attention" + "value": "%@ didn't resume after waking." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "يحتاج إلى انتباه" + "value": "%@ はスリープ解除後に再開しませんでした。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Erfordert Aufmerksamkeit" + "value": "%@ wurde nach dem Aufwecken nicht fortgesetzt." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Requiere atención" + "value": "%@ n’a pas repris après le réveil." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Nécessite une attention" + "value": "لم يُستأنف %@ بعد الإيقاظ." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "対応が必要" + "value": "%@ no se reanudó después de despertar." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "확인 필요" + "value": "%@ 在喚醒後未能恢復。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需要处理" + "value": "%@ 在唤醒后未能恢复。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "需要處理" + "value": "%@이(가) 깨운 후 다시 시작되지 않았습니다." } } } }, - "menu.cloud.title": { + "agentWake.banner.retry": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "Retry" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "السحابة" + "value": "再試行" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "Erneut versuchen" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "Réessayer" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Cloud" + "value": "إعادة المحاولة" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "クラウド" + "value": "Reintentar" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "클라우드" + "value": "重試" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端" + "value": "重试" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "雲端" + "value": "재시도" } } } }, - "cloudMenu.machines.loading": { + "agentWake.banner.showCommand": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud Machines · Loading…" + "value": "Show command" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "أجهزة السحابة · جارٍ التحميل…" + "value": "コマンドを表示" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen · Wird geladen…" + "value": "Befehl anzeigen" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Máquinas de Cloud · Cargando…" + "value": "Afficher la commande" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Machines Cloud · Chargement…" + "value": "إظهار الأمر" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "クラウドマシン · 読み込み中…" + "value": "Mostrar comando" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "클라우드 머신 · 불러오는 중…" + "value": "顯示指令" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端机器 · 正在加载…" + "value": "显示命令" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "雲端機器 · 正在載入…" + "value": "명령 보기" } } } }, - "cloudMenu.machines.showAll": { + "agentWake.notification.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Show All Machines…" + "value": "%@ didn't resume" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "عرض كل الأجهزة…" + "value": "%@ が再開しませんでした" } }, "de": { "stringUnit": { "state": "translated", - "value": "Alle Maschinen anzeigen…" + "value": "%@ wurde nicht fortgesetzt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ n’a pas repris" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Mostrar todas las máquinas…" + "value": "%@ no se reanudó" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Afficher toutes les machines…" + "value": "%@ 未能恢復" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "すべてのマシンを表示…" + "value": "%@ 未能恢复" } }, "ko": { "stringUnit": { "state": "translated", - "value": "모든 머신 보기…" + "value": "%@이(가) 다시 시작되지 않았습니다" + } + } + } + }, + "agentWake.reason.didNotStart": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "It did not start within 90 seconds." } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "显示所有机器…" + "value": "90 秒以内に起動しませんでした。" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Es wurde nicht innerhalb von 90 Sekunden gestartet." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n’a pas démarré dans les 90 secondes." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يبدأ خلال 90 ثانية." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se inició en 90 segundos." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "顯示所有機器…" + "value": "未在 90 秒內啟動。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "未在 90 秒内启动。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "90초 안에 시작되지 않았습니다." } } } }, - "cloudMenu.newMachine": { + "agentWake.reason.exitedBeforeStart": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "New Cloud Machine…" + "value": "Its resume command exited." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "جهاز سحابي جديد…" + "value": "再開コマンドが終了しました。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Neue Cloud-Maschine…" + "value": "Sein Fortsetzungsbefehl wurde beendet." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sa commande de reprise s’est terminée." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "انتهى أمر الاستئناف الخاص به." } }, "es": { "stringUnit": { "state": "translated", - "value": "Nueva máquina de Cloud…" + "value": "Su comando de reanudación terminó." } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Nouvelle machine Cloud…" + "value": "其恢復指令已結束。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "新規クラウドマシン…" + "value": "其恢复命令已退出。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "새 클라우드 머신…" + "value": "재개 명령이 종료되었습니다." + } + } + } + }, + "agentWake.status.multiple": { + "extractionState": "manual", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%ld agent didn't resume" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld agents didn't resume" + } + } + } + } + }, + "ja": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld 個のエージェントが再開しませんでした" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%ld Agent wurde nicht fortgesetzt" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld Agenten wurden nicht fortgesetzt" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%ld agent n’a pas repris" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%ld agents n’ont pas repris" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld agents n’ont pas repris" + } + } + } + } + }, + "ar": { + "variations": { + "plural": { + "zero": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف أي وكيل (%ld)" + } + }, + "one": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف وكيل واحد (%ld)" + } + }, + "two": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف وكيلان (%ld)" + } + }, + "few": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف %ld وكلاء" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف %ld وكيلًا" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف %ld وكيل" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%ld agente no se reanudó" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%ld agentes no se reanudaron" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld agentes no se reanudaron" + } + } + } + } + }, + "zh-Hant": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld 個代理程式未能恢復" + } + } + } } }, "zh-Hans": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%ld 个代理未能恢复" + } + } + } + } + }, + "ko": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "에이전트 %ld개가 다시 시작되지 않았습니다" + } + } + } + } + } + } + }, + "agentWake.status.single": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "新建云端机器…" + "value": "Agent didn't resume" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントが再開しませんでした" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agent wurde nicht fortgesetzt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’agent n’a pas repris" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يُستأنف الوكيل" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El agente no se reanudó" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "新增雲端機器…" + "value": "代理程式未能恢復" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代理未能恢复" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트가 다시 시작되지 않았습니다" } } } }, - "cloudMenu.signIn": { + "cli.help.record": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sign In to cmux Cloud…" + "value": "Usage: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nRecord a cmux window to an mp4 or gif, for a pull request or a bug\nreport. Only cmux's own windows can be recorded, and no Screen\nRecording permission is involved.\n\nFlags for start:\n --format mp4|gif Output format (default: mp4)\n --gif Shorthand for --format gif\n --fps <1-30> Frames per second (default: 12 for mp4, 8 for gif)\n --max-seconds Stop by itself after n seconds, 0.5-120 (default: 15)\n --scale <0.1-1> Scale the output (default: 1 for mp4, 0.5 for gif)\n --max-width <64-4096> Cap the output width, keeping the aspect ratio\n --region Record part of the window, in window points\n --out Write here instead of a temporary directory\n --label Name the file\n --no-captions Do not draw `record note` captions into the clip\n --window Window to record (default: the frontmost one)\n\nOutput: ` `, or the full response with --json.\n\nA recording stops itself at --max-seconds, so a clip is never left\nrunning by an agent that goes away. One recording at a time.\n\nExample:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تسجيل الدخول إلى cmux Cloud…" + "value": "الاستخدام: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nتسجيل نافذة cmux بصيغة mp4 أو gif، من أجل طلب سحب أو تقرير خطأ.\nلا يمكن تسجيل سوى نوافذ cmux نفسها،\nولا يتطلب ذلك إذن تسجيل الشاشة.\n\nخيارات start:\n --format mp4|gif صيغة الإخراج (الافتراضي: mp4)\n --gif اختصار لـ --format gif\n --fps <1-30> الإطارات في الثانية (الافتراضي: 12 لـ mp4، 8 لـ gif)\n --max-seconds يتوقف من تلقاء نفسه بعد n ثانية، 0.5-120 (الافتراضي: 15)\n --scale <0.1-1> تحجيم الإخراج (الافتراضي: 1 لـ mp4، 0.5 لـ gif)\n --max-width <64-4096> تحديد أقصى عرض للإخراج مع الحفاظ على نسبة الأبعاد\n --region تسجيل جزء من النافذة، بوحدات نقاط النافذة\n --out الكتابة هنا بدلاً من دليل مؤقت\n --label تسمية الملف\n --no-captions عدم رسم تعليقات `record note` داخل المقطع\n --window النافذة المراد تسجيلها (الافتراضي: النافذة الأمامية)\n\nالإخراج: ` `، أو الاستجابة الكاملة مع --json.\n\nيتوقف التسجيل من تلقاء نفسه عند --max-seconds، حتى لا يترك وكيل غادر\nمقطعًا قيد التسجيل. تسجيل واحد في كل مرة.\n\nمثال:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "de": { "stringUnit": { "state": "translated", - "value": "Bei cmux Cloud anmelden…" + "value": "Verwendung: cmux record [start] [Flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nEin cmux-Fenster als mp4 oder gif aufzeichnen, für einen Pull Request\noder einen Fehlerbericht. Nur cmux-eigene Fenster können aufgezeichnet\nwerden, und es ist keine Bildschirmaufnahme-Berechtigung beteiligt.\n\nFlags für start:\n --format mp4|gif Ausgabeformat (Standard: mp4)\n --gif Kurzform für --format gif\n --fps <1-30> Bilder pro Sekunde (Standard: 12 für mp4, 8 für gif)\n --max-seconds Nach n Sekunden selbst beenden, 0.5-120 (Standard: 15)\n --scale <0.1-1> Ausgabe skalieren (Standard: 1 für mp4, 0.5 für gif)\n --max-width <64-4096> Ausgabebreite begrenzen, Seitenverhältnis bleibt\n --region Teil des Fensters aufzeichnen, in Fensterpunkten\n --out Hierhin schreiben statt in ein temporäres Verzeichnis\n --label Datei benennen\n --no-captions Keine `record note`-Untertitel in den Clip zeichnen\n --window Fenster zum Aufzeichnen (Standard: das vorderste)\n\nAusgabe: ` `, oder mit --json die vollständige Antwort.\n\nEine Aufzeichnung beendet sich bei --max-seconds selbst, damit ein Agent, der\nverschwindet, keinen laufenden Clip hinterlässt. Eine Aufzeichnung zur Zeit.\n\nBeispiel:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "es": { "stringUnit": { "state": "translated", - "value": "Iniciar sesión en cmux Cloud…" + "value": "Uso: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nGraba una ventana de cmux en mp4 o gif, para un pull request o un informe\nde error. Solo se pueden grabar las ventanas propias de cmux, y no\ninterviene ningún permiso de grabación de pantalla.\n\nFlags para start:\n --format mp4|gif Formato de salida (predeterminado: mp4)\n --gif Atajo para --format gif\n --fps <1-30> Fotogramas por segundo (predeterminado: 12 para mp4, 8 para gif)\n --max-seconds Se detiene solo tras n segundos, 0.5-120 (predeterminado: 15)\n --scale <0.1-1> Escala la salida (predeterminado: 1 para mp4, 0.5 para gif)\n --max-width <64-4096> Limita el ancho de salida manteniendo la proporción\n --region Graba parte de la ventana, en puntos de ventana\n --out Escribe aquí en lugar de en un directorio temporal\n --label Da nombre al archivo\n --no-captions No dibujar los subtítulos de `record note` en el clip\n --window Ventana que se graba (predeterminado: la del frente)\n\nSalida: ` `, o la respuesta completa con --json.\n\nUna grabación se detiene sola en --max-seconds, para que un agente que\ndesaparece nunca deje un clip en marcha. Una grabación a la vez.\n\nEjemplo:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Se connecter à cmux Cloud…" + "value": "Utilisation : cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nEnregistrer une fenêtre cmux en mp4 ou gif, pour une pull request ou un\nrapport de bug. Seules les fenêtres de cmux peuvent être enregistrées, et\naucune autorisation d'enregistrement de l'écran n'intervient.\n\nFlags pour start :\n --format mp4|gif Format de sortie (par défaut : mp4)\n --gif Raccourci pour --format gif\n --fps <1-30> Images par seconde (par défaut : 12 pour mp4, 8 pour gif)\n --max-seconds S'arrête seul après n secondes, 0.5-120 (par défaut : 15)\n --scale <0.1-1> Mettre la sortie à l'échelle (par défaut : 1 pour mp4, 0.5 pour gif)\n --max-width <64-4096> Limiter la largeur de sortie en gardant les proportions\n --region N'enregistrer qu'une partie de la fenêtre, en points fenêtre\n --out Écrire ici au lieu d'un répertoire temporaire\n --label Nommer le fichier\n --no-captions Ne pas dessiner les légendes `record note` dans le clip\n --window Fenêtre à enregistrer (par défaut : celle au premier plan)\n\nSortie : ` `, ou la réponse complète avec --json.\n\nUn enregistrement s'arrête de lui-même à --max-seconds, afin qu'un agent qui\ndisparaît ne laisse jamais un clip en cours. Un seul enregistrement à la fois.\n\nExemple :\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux Cloud にサインイン…" + "value": "使用方法: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nプルリクエストやバグ報告のために、cmux のウィンドウを mp4 または gif\nで録画します。録画できるのは cmux 自身のウィンドウだけで、画面収録の\n権限は必要ありません。\n\nstart のフラグ:\n --format mp4|gif 出力形式(既定: mp4)\n --gif --format gif の短縮形\n --fps <1-30> 1 秒あたりのフレーム数(既定: mp4 は 12、gif は 8)\n --max-seconds n 秒後に自動停止、0.5-120(既定: 15)\n --scale <0.1-1> 出力を拡大縮小(既定: mp4 は 1、gif は 0.5)\n --max-width <64-4096> 縦横比を保ったまま出力の幅を制限\n --region ウィンドウの一部だけを録画(ウィンドウポイント単位)\n --out 一時ディレクトリではなくこのパスに書き出す\n --label ファイルに名前を付ける\n --no-captions `record note` のキャプションをクリップに描画しない\n --window 録画するウィンドウ(既定: 最前面のウィンドウ)\n\n出力: ` `、または --json で完全なレスポンス。\n\n録画は --max-seconds で自動停止するので、いなくなったエージェントが\n録画を実行したままにすることはありません。録画は同時に 1 つだけです。\n\n例:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux Cloud에 로그인…" + "value": "사용법: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\n풀 리퀘스트나 버그 리포트에 쓰려고 cmux 창을 mp4 또는 gif로\n녹화합니다. cmux 자신의 창만 녹화할 수 있고, 화면 기록 권한은\n필요하지 않습니다.\n\nstart 플래그:\n --format mp4|gif 출력 형식 (기본값: mp4)\n --gif --format gif의 단축형\n --fps <1-30> 초당 프레임 수 (기본값: mp4는 12, gif는 8)\n --max-seconds n초 후 스스로 정지, 0.5-120 (기본값: 15)\n --scale <0.1-1> 출력 크기 조정 (기본값: mp4는 1, gif는 0.5)\n --max-width <64-4096> 가로 세로 비율을 유지하며 출력 너비 제한\n --region 창의 일부만 녹화, 창 포인트 단위\n --out 임시 디렉터리 대신 이 경로에 기록\n --label 파일 이름 지정\n --no-captions `record note` 캡션을 클립에 그리지 않음\n --window 녹화할 창 (기본값: 맨 앞 창)\n\n출력: ` `, 또는 --json으로 전체 응답.\n\n녹화는 --max-seconds에서 스스로 멈추므로 사라진 에이전트가 녹화를 계속\n남겨 두는 일은 없습니다. 한 번에 하나의 녹화만 가능합니다.\n\n예:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "登录 cmux Cloud…" + "value": "使用方法: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\n将 cmux 窗口录制为 mp4 或 gif,用于 pull request 或错误报告。\n只能录制 cmux 自己的窗口,\n而且不需要任何屏幕录制权限。\n\nstart 的标志:\n --format mp4|gif 输出格式(默认: mp4)\n --gif --format gif 的简写\n --fps <1-30> 每秒帧数(默认: mp4 为 12,gif 为 8)\n --max-seconds n 秒后自行停止,0.5-120(默认: 15)\n --scale <0.1-1> 缩放输出(默认: mp4 为 1,gif 为 0.5)\n --max-width <64-4096> 限制输出宽度并保持宽高比\n --region 只录制窗口的一部分,以窗口点为单位\n --out 写入此处而非临时目录\n --label 为文件命名\n --no-captions 不要把 `record note` 字幕画进视频\n --window 要录制的窗口(默认: 最前面的窗口)\n\n输出: ` `,或用 --json 获取完整响应。\n\n录制会在 --max-seconds 时自行停止,因此离开的代理不会留下仍在录制的\n视频。同一时间只能有一份录制。\n\n示例:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "登入 cmux Cloud…" + "value": "使用方法: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\n將 cmux 視窗錄製成 mp4 或 gif,用於 pull request 或錯誤報告。\n只能錄製 cmux 自己的視窗,\n而且不需要任何螢幕錄製權限。\n\nstart 的旗標:\n --format mp4|gif 輸出格式(預設: mp4)\n --gif --format gif 的簡寫\n --fps <1-30> 每秒影格數(預設: mp4 為 12,gif 為 8)\n --max-seconds n 秒後自行停止,0.5-120(預設: 15)\n --scale <0.1-1> 縮放輸出(預設: mp4 為 1,gif 為 0.5)\n --max-width <64-4096> 限制輸出寬度並維持長寬比\n --region 只錄製視窗的一部分,以視窗點為單位\n --out 寫入此處而非暫存目錄\n --label 為檔案命名\n --no-captions 不要將 `record note` 字幕畫進影片\n --window 要錄製的視窗(預設: 最前面的視窗)\n\n輸出: ` `,或以 --json 取得完整回應。\n\n錄製會在 --max-seconds 時自行停止,因此離開的代理程式不會留下仍在\n錄製的影片。同一時間只能有一份錄製。\n\n範例:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" } } } }, - "cloudMenu.signInAgain": { + "cli.record.error.formatConflict": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sign In Again…" + "value": "record start: --gif conflicts with --format" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تسجيل الدخول مجددًا…" + "value": "record start: لا يمكن دمج --gif مع --format" } }, "de": { "stringUnit": { "state": "translated", - "value": "Erneut anmelden…" + "value": "record start: --gif kann nicht mit --format kombiniert werden" } }, "es": { "stringUnit": { "state": "translated", - "value": "Volver a iniciar sesión…" + "value": "record start: --gif no se puede combinar con --format" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Se reconnecter…" + "value": "record start: --gif ne peut pas être combiné avec --format" } }, "ja": { "stringUnit": { "state": "translated", - "value": "もう一度サインイン…" + "value": "record start: --gif は --format と併用できません" } }, "ko": { "stringUnit": { "state": "translated", - "value": "다시 로그인…" + "value": "record start: --gif은 --format과 함께 사용할 수 없습니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "重新登录…" + "value": "record start: --gif 不能与 --format 同用" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "重新登入…" + "value": "record start: --gif 無法與 --format 同時使用" } } } }, - "cloudMenu.signingIn": { + "cli.record.error.noteText": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Signing In…" + "value": "record note: text is required" } }, "ar": { "stringUnit": { "state": "translated", - "value": "جارٍ تسجيل الدخول…" + "value": "record note: النص مطلوب" } }, "de": { "stringUnit": { "state": "translated", - "value": "Anmeldung läuft…" + "value": "record note: Text ist erforderlich" } }, "es": { "stringUnit": { "state": "translated", - "value": "Iniciando sesión…" + "value": "record note: se requiere el texto" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Connexion…" + "value": "record note : le texte est requis" } }, "ja": { "stringUnit": { "state": "translated", - "value": "サインイン中…" + "value": "record note: テキストが必要です" } }, "ko": { "stringUnit": { "state": "translated", - "value": "로그인 중…" + "value": "record note: 텍스트가 필요합니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "正在登录…" + "value": "record note: 必须提供文本" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "正在登入…" + "value": "record note: 必須提供文字" } } } }, - "cloudMenu.upgrade": { + "cli.record.error.unexpectedArguments": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Upgrade to Pro…" + "value": "record %@: unexpected arguments: %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الترقية إلى Pro…" + "value": "record %@: وسائط غير متوقعة: %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Auf Pro upgraden…" + "value": "record %@: unerwartete Argumente: %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Mejorar a Pro…" + "value": "record %@: argumentos inesperados: %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Passer à Pro…" + "value": "record %@ : arguments inattendus : %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Pro にアップグレード…" + "value": "record %@: 予期しない引数: %@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Pro로 업그레이드…" + "value": "record %@: 예기치 않은 인수: %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "升级到 Pro…" + "value": "record %@: 多余的参数: %@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "升級至 Pro…" + "value": "record %@: 未預期的引數: %@" } } } }, - "terminalContextMenu.copyLink": { + "cli.record.error.unknownSubcommand": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Copy Link" + "value": "record: unknown subcommand '%@' (start, stop, status, note, list)" } }, "ar": { "stringUnit": { "state": "translated", - "value": "نسخ الرابط" + "value": "record: أمر فرعي غير معروف '%@' (start، stop، status، note، list)" } }, "de": { "stringUnit": { "state": "translated", - "value": "Link kopieren" + "value": "record: unbekannter Unterbefehl „%@“ (start, stop, status, note, list)" } }, "es": { "stringUnit": { "state": "translated", - "value": "Copiar enlace" + "value": "record: subcomando desconocido '%@' (start, stop, status, note, list)" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Copier le lien" + "value": "record : sous-commande inconnue '%@' (start, stop, status, note, list)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "リンクをコピー" + "value": "record: 不明なサブコマンド '%@' です(start、stop、status、note、list)" } }, "ko": { "stringUnit": { "state": "translated", - "value": "링크 복사" + "value": "record: 알 수 없는 하위 명령 '%@' (start, stop, status, note, list)" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "复制链接" + "value": "record: 未知的子命令 '%@'(start、stop、status、note、list)" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "複製連結" + "value": "record: 未知的子命令 '%@'(start、stop、status、note、list)" } } } }, - "terminalContextMenu.openLinkInCmuxBrowser": { + "cli.usage.record": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "Open Link in cmux Browser" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "فتح الرابط في متصفح cmux" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, - "de": { + "en": { "stringUnit": { "state": "translated", - "value": "Link im cmux-Browser öffnen" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, "es": { "stringUnit": { "state": "translated", - "value": "Abrir enlace en el navegador de cmux" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ouvrir le lien dans le navigateur cmux" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, "ja": { "stringUnit": { "state": "translated", - "value": "リンクをcmuxブラウザで開く" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, "ko": { "stringUnit": { "state": "translated", - "value": "링크를 cmux 브라우저에서 열기" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 cmux 浏览器中打开链接" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 cmux 瀏覽器中開啟連結" + "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" } } } }, - "terminalContextMenu.openLinkInDefaultBrowser": { + "cli.help.shot": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Open Link in Default Browser" + "value": "Usage: cmux shot [flags]\n\nScreenshot a cmux window, or a region of one, to a png or a jpeg. Only\ncmux's own windows can be captured, and no Screen Recording permission\nis involved, so this works on a release build and inside CI.\n\nFlags:\n --format png|jpg Output format (default: png)\n --jpg Shorthand for --format jpg\n --scale <0.1-1> Scale the output (default: 1)\n --max-width <64-8192> Cap the output width, keeping the aspect ratio\n --quality <0.1-1> JPEG quality (default: 0.8, ignored for png)\n --region Capture part of the window, in window points\n --out Write here instead of a temporary directory\n --label Name the file\n --caption Draw a caption into the image\n --window Window to capture (default: the frontmost one)\n\nOutput: `x `, or the full response with --json.\n\nA region is the same rectangle `cmux record --region` films, so a\ndetail spotted in a clip can be shot with the same four numbers.\n\nExample:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "ar": { "stringUnit": { "state": "translated", - "value": "فتح الرابط في المتصفح الافتراضي" + "value": "الاستخدام: cmux shot [flags]\n\nتصوير نافذة cmux أو جزء منها بصيغة png أو jpeg.\nلا يمكن تصوير سوى نوافذ cmux نفسها، ولا يتطلب ذلك إذن تسجيل الشاشة،\nلذلك يعمل هذا في بناء الإصدار وداخل CI.\n\nالخيارات:\n --format png|jpg صيغة الإخراج (الافتراضي: png)\n --jpg اختصار لـ --format jpg\n --scale <0.1-1> تحجيم الإخراج (الافتراضي: 1)\n --max-width <64-8192> تحديد أقصى عرض للإخراج مع الحفاظ على نسبة الأبعاد\n --quality <0.1-1> جودة JPEG (الافتراضي: 0.8، وتُهمل مع png)\n --region تصوير جزء من النافذة، بوحدات نقاط النافذة\n --out الكتابة هنا بدلاً من دليل مؤقت\n --label تسمية الملف\n --caption رسم تعليق داخل الصورة\n --window النافذة المراد تصويرها (الافتراضي: النافذة الأمامية)\n\nالإخراج: `x `، أو الاستجابة الكاملة مع --json.\n\nالمنطقة هي نفس المستطيل الذي يصوّره `cmux record --region`، لذا يمكن تصوير\nتفصيل لوحظ في مقطع بالأرقام الأربعة نفسها.\n\nمثال:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "de": { "stringUnit": { "state": "translated", - "value": "Link im Standardbrowser öffnen" + "value": "Verwendung: cmux shot [Flags]\n\nEin cmux-Fenster oder einen Teil davon als png oder jpeg aufnehmen. Nur\ncmux-eigene Fenster sind aufnehmbar, und es ist keine Berechtigung für\nBildschirmaufnahmen nötig, das geht also im Release-Build und in der CI.\n\nFlags:\n --format png|jpg Ausgabeformat (Standard: png)\n --jpg Kurzform für --format jpg\n --scale <0.1-1> Ausgabe skalieren (Standard: 1)\n --max-width <64-8192> Ausgabebreite begrenzen, Seitenverhältnis bleibt\n --quality <0.1-1> JPEG-Qualität (Standard: 0.8, für png ignoriert)\n --region Teil des Fensters aufnehmen, in Fensterpunkten\n --out Hierhin schreiben statt in ein temporäres Verzeichnis\n --label Datei benennen\n --caption Eine Bildunterschrift ins Bild zeichnen\n --window Fenster zum Aufnehmen (Standard: das vorderste)\n\nAusgabe: `x `, oder mit --json die vollständige Antwort.\n\nEine Region ist dasselbe Rechteck, das `cmux record --region` filmt: ein im\nClip entdecktes Detail lässt sich mit denselben vier Zahlen aufnehmen.\n\nBeispiel:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "es": { "stringUnit": { "state": "translated", - "value": "Abrir enlace en el navegador predeterminado" + "value": "Uso: cmux shot [flags]\n\nCaptura una ventana de cmux, o una región de ella, en png o jpeg. Solo se\npueden capturar las ventanas propias de cmux, y no interviene ningún permiso\nde grabación de pantalla, así que funciona en una release y dentro de CI.\n\nFlags:\n --format png|jpg Formato de salida (predeterminado: png)\n --jpg Atajo para --format jpg\n --scale <0.1-1> Escala la salida (predeterminado: 1)\n --max-width <64-8192> Limita el ancho de salida manteniendo la proporción\n --quality <0.1-1> Calidad JPEG (predeterminado: 0.8, se ignora en png)\n --region Captura parte de la ventana, en puntos de ventana\n --out Escribe aquí en lugar de en un directorio temporal\n --label Da nombre al archivo\n --caption Dibuja un rótulo dentro de la imagen\n --window Ventana que se captura (predeterminado: la del frente)\n\nSalida: `x `, o la respuesta completa con --json.\n\nUna región es el mismo rectángulo que filma `cmux record --region`, así que un\ndetalle visto en un clip se captura con los mismos cuatro números.\n\nEjemplo:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ouvrir le lien dans le navigateur par défaut" + "value": "Utilisation : cmux shot [flags]\n\nCapture une fenêtre cmux, ou une partie de celle-ci, en png ou en jpeg.\nSeules les fenêtres de cmux peuvent être capturées, et aucune autorisation\nd'enregistrement d'écran n'intervient : cela marche en release et dans la CI.\n\nFlags :\n --format png|jpg Format de sortie (par défaut : png)\n --jpg Raccourci pour --format jpg\n --scale <0.1-1> Met la sortie à l'échelle (par défaut : 1)\n --max-width <64-8192> Limite la largeur de sortie en gardant les proportions\n --quality <0.1-1> Qualité JPEG (par défaut : 0.8, ignorée pour png)\n --region Capture une partie de la fenêtre, en points de fenêtre\n --out Écrit ici au lieu d'un répertoire temporaire\n --label Nomme le fichier\n --caption Dessine une légende dans l'image\n --window Fenêtre à capturer (par défaut : celle au premier plan)\n\nSortie : `x `, ou la réponse complète avec --json.\n\nUne région est le même rectangle que filme `cmux record --region` : un détail\nrepéré dans un clip se capture avec les mêmes quatre nombres.\n\nExemple :\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "ja": { "stringUnit": { "state": "translated", - "value": "リンクをデフォルトブラウザで開く" + "value": "使用方法: cmux shot [flags]\n\ncmux のウィンドウ、またはその一部の領域を png または jpeg で撮ります。\n撮れるのは cmux 自身のウィンドウだけで、画面収録の権限は必要ないので、\nリリースビルドでも CI の中でも動きます。\n\nフラグ:\n --format png|jpg 出力形式(既定: png)\n --jpg --format jpg の短縮形\n --scale <0.1-1> 出力を拡大縮小(既定: 1)\n --max-width <64-8192> 縦横比を保ったまま出力の幅を制限\n --quality <0.1-1> JPEG の品質(既定: 0.8、png では無視)\n --region ウィンドウの一部だけを撮る(ウィンドウポイント単位)\n --out 一時ディレクトリではなくこのパスに書き出す\n --label ファイルに名前を付ける\n --caption 画像にキャプションを描画する\n --window 撮るウィンドウ(既定: 最前面のウィンドウ)\n\n出力: `x `、または --json で完全なレスポンス。\n\n領域は `cmux record --region` が撮るのと同じ矩形なので、クリップで見つけた\n細部を同じ 4 つの数字で撮れます。\n\n例:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "ko": { "stringUnit": { "state": "translated", - "value": "링크를 기본 브라우저에서 열기" + "value": "사용법: cmux shot [flags]\n\ncmux 창이나 창의 일부 영역을 png 또는 jpeg로 캡처합니다. cmux 자신의\n창만 캡처할 수 있고, 화면 기록 권한이 필요하지 않으므로 릴리스 빌드와\nCI 안에서도 동작합니다.\n\n플래그:\n --format png|jpg 출력 형식 (기본값: png)\n --jpg --format jpg의 단축형\n --scale <0.1-1> 출력 크기 조정 (기본값: 1)\n --max-width <64-8192> 가로 세로 비율을 유지하며 출력 너비 제한\n --quality <0.1-1> JPEG 품질 (기본값: 0.8, png에서는 무시)\n --region 창의 일부만 캡처, 창 포인트 단위\n --out 임시 디렉터리 대신 이 경로에 기록\n --label 파일 이름 지정\n --caption 이미지에 캡션 그리기\n --window 캡처할 창 (기본값: 맨 앞 창)\n\n출력: `x `, 또는 --json으로 전체 응답.\n\n영역은 `cmux record --region`이 촬영하는 사각형과 같으므로, 클립에서 본\n세부를 같은 네 숫자로 캡처할 수 있습니다.\n\n예:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在默认浏览器中打开链接" + "value": "使用方法: cmux shot [flags]\n\n将 cmux 窗口或其中一块区域截取为 png 或 jpeg。\n只能截取 cmux 自己的窗口,而且不需要任何屏幕录制权限,\n因此在 release 构建和 CI 中都能用。\n\n标志:\n --format png|jpg 输出格式(默认: png)\n --jpg --format jpg 的简写\n --scale <0.1-1> 缩放输出(默认: 1)\n --max-width <64-8192> 限制输出宽度并保持宽高比\n --quality <0.1-1> JPEG 质量(默认: 0.8,png 会忽略)\n --region 只截取窗口的一部分,以窗口点为单位\n --out 写入此处而非临时目录\n --label 为文件命名\n --caption 在图片上绘制字幕\n --window 要截取的窗口(默认: 最前面的窗口)\n\n输出: `x `,或用 --json 获取完整响应。\n\n区域和 `cmux record --region` 拍摄的矩形相同,因此在视频里看到的细节\n可以用同样的四个数字截取。\n\n示例:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在預設瀏覽器中開啟連結" + "value": "使用方法: cmux shot [flags]\n\n將 cmux 視窗或其中一塊區域擷取成 png 或 jpeg。\n只能擷取 cmux 自己的視窗,而且不需要任何螢幕錄製權限,\n因此在 release 版本與 CI 內都能用。\n\n旗標:\n --format png|jpg 輸出格式(預設: png)\n --jpg --format jpg 的簡寫\n --scale <0.1-1> 縮放輸出(預設: 1)\n --max-width <64-8192> 限制輸出寬度並保持長寬比\n --quality <0.1-1> JPEG 品質(預設: 0.8,png 會忽略)\n --region 只擷取視窗的一部分,以視窗點為單位\n --out 寫入此處而非暫存目錄\n --label 為檔案命名\n --caption 在圖片上畫出字幕\n --window 要擷取的視窗(預設: 最前面的視窗)\n\n輸出: `x `,或用 --json 取得完整回應。\n\n區域和 `cmux record --region` 拍攝的矩形相同,所以在影片裡看到的細節\n可以用同樣的四個數字擷取。\n\n範例:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" } } } }, - "cloudTree.menu.rename": { + "cli.shot.error.formatConflict": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Rename…" + "value": "shot: --jpg conflicts with --format" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "名称変更…" + "value": "shot: لا يمكن دمج --jpg مع --format" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "重命名…" + "value": "shot: --jpg kann nicht mit --format kombiniert werden" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "Umbenennen…" + "value": "shot: --jpg no se puede combinar con --format" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Renommer…" + "value": "shot : --jpg ne peut pas être combiné avec --format" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "إعادة التسمية…" + "value": "shot: --jpg は --format と併用できません" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Renombrar…" + "value": "shot: --jpg은 --format과 함께 사용할 수 없습니다" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "重新命名…" + "value": "shot: --jpg 不能与 --format 同用" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이름 바꾸기…" + "value": "shot: --jpg 無法與 --format 同時使用" } } } }, - "cloudTree.operation.rename": { + "cli.shot.error.unexpectedArguments": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Renaming %@…" + "value": "shot: unexpected arguments: %@" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "%@ の名称を変更しています…" + "value": "shot: وسائط غير متوقعة: %@" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "正在重命名 %@…" + "value": "shot: unerwartete Argumente: %@" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "%@ wird umbenannt…" + "value": "shot: argumentos inesperados: %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Renommage de %@…" + "value": "shot : arguments inattendus : %@" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "جاري إعادة تسمية %@…" + "value": "shot: 予期しない引数: %@" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Renombrando %@…" + "value": "shot: 예기치 않은 인수: %@" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "正在重新命名 %@…" + "value": "shot: 多余的参数: %@" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%@ 이름 바꾸는 중…" + "value": "shot: 未預期的引數: %@" } } } }, - "cloudTree.operation.clearName": { + "cli.usage.shot": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Clearing %@…" + "value": "shot [--format png|jpg] [--jpg] [--scale ] [--max-width ] [--quality ] [--region ] [--out ] [--label ] [--caption ] [--window ]" } }, "ja": { "stringUnit": { "state": "translated", - "value": "%@ のカスタム名をクリアしています…" + "value": "shot [--format png|jpg] [--jpg] [--scale ] [--max-width ] [--quality ] [--region ] [--out ] [--label ] [--caption ] [--window ]" + } + } + } + }, + "socket.sidebar.closeSurface.confirmationRequired": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Surface has a running process; retry with --force" } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "正在清空 %@…" + "value": "تحتوي السطح على عملية قيد التشغيل؛ أعد المحاولة باستخدام --force" } }, "de": { "stringUnit": { "state": "translated", - "value": "%@ wird geleert…" + "value": "Die Oberfläche hat einen laufenden Prozess; erneut mit --force versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La superficie tiene un proceso en ejecución; vuelve a intentarlo con --force" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Effacement de %@…" + "value": "La surface a un processus en cours d’exécution ; réessayez avec --force" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "جاري مسح %@…" + "value": "サーフェスでプロセスが実行中です。--force を指定して再試行してください" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Limpiando %@…" + "value": "표면에서 실행 중인 프로세스가 있습니다. --force로 다시 시도하세요" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "正在清除 %@…" + "value": "此表面有正在运行的进程;请使用 --force 重试" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%@ 비우는 중…" + "value": "此表面有正在執行的程序;請使用 --force 重試" } } } }, - "cloudTree.rename.title": { + "socket.sidebar.closeSurface.failed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Rename “%@”" + "value": "Failed to close surface" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "「%@」の名称変更" + "value": "تعذر إغلاق السطح" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "重命名“%@”" + "value": "Die Oberfläche konnte nicht geschlossen werden" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "„%@“ umbenennen" + "value": "No se pudo cerrar la superficie" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Renommer « %@ »" + "value": "Échec de la fermeture de la surface" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "إعادة تسمية “%@”" + "value": "サーフェスを閉じられませんでした" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Renombrar “%@”" + "value": "표면을 닫지 못했습니다" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "重新命名「%@」" + "value": "无法关闭此表面" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "“%@” 이름 바꾸기" + "value": "無法關閉表面" } } } }, - "cloudTree.error.renameNoRemoteTab": { + "socket.surface.close.confirmationRequired": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This tab is not open in a remote workspace." + "value": "Surface has a running process; retry with force=true" } }, "ar": { "stringUnit": { "state": "translated", - "value": "علامة التبويب هذه ليست مفتوحة في مساحة عمل بعيدة." + "value": "تحتوي السطح على عملية قيد التشغيل؛ أعد المحاولة باستخدام force=true" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Tab ist in keinem Remote-Arbeitsbereich geöffnet." + "value": "Die Oberfläche hat einen laufenden Prozess; erneut mit force=true versuchen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Esta pestaña no está abierta en un espacio de trabajo remoto." + "value": "La superficie tiene un proceso en ejecución; vuelve a intentarlo con force=true" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Cet onglet n’est pas ouvert dans un espace de travail distant." + "value": "La surface a un processus en cours d’exécution ; réessayez avec force=true" } }, "ja": { "stringUnit": { "state": "translated", - "value": "このタブはリモートワークスペースで開かれていません。" + "value": "サーフェスでプロセスが実行中です。force=true を指定して再試行してください" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 탭은 원격 작업 공간에 열려 있지 않습니다." + "value": "표면에서 실행 중인 프로세스가 있습니다. force=true로 다시 시도하세요" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此标签页未在任何远程工作区中打开。" + "value": "此表面有正在运行的进程;请使用 force=true 重试" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此標籤頁未在遠端工作區中開啟。" + "value": "此表面有正在執行的程序;請使用 force=true 重試" } } } }, - "cloudTree.error.renameTabUnavailable": { + "socket.surface.close.failed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The current state for tab %@ is unavailable. Refresh and retry before renaming." + "value": "Failed to close surface" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Der aktuelle Zustand von Tab %@ ist nicht verfügbar. Aktualisieren Sie die Ansicht und versuchen Sie die Umbenennung erneut." + "value": "تعذر إغلاق السطح" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "L’état actuel de l’onglet %@ n’est pas disponible. Actualisez la page et réessayez de le renommer." + "value": "Die Oberfläche konnte nicht geschlossen werden" } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "الحالة الحالية لعلامة التبويب %@ غير متاحة. حدّث الصفحة وحاول إعادة التسمية مرة أخرى." + "value": "No se pudo cerrar la superficie" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "El estado actual de la pestaña %@ no está disponible. Actualiza la página y vuelve a intentar cambiarle el nombre." + "value": "Échec de la fermeture de la surface" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "標籤頁 %@ 的目前狀態無法使用。請重新整理後再試一次重新命名。" + "value": "サーフェスを閉じられませんでした" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "标签页 %@ 的当前状态不可用。请刷新后重试重命名。" + "value": "표면을 닫지 못했습니다" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "탭 %@의 현재 상태를 사용할 수 없습니다. 새로 고친 후 이름을 다시 바꾸십시오." + "value": "无法关闭此表面" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "タブ %@ の現在の状態を取得できません。更新してから名前の変更をもう一度お試しください。" + "value": "無法關閉表面" } } } }, - "cli.restore.error.routedLauncherNotFound": { + "socket.tabAction.close.confirmationRequired": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "restore: this session was started with '%1$@ claude proxy', but '%1$@' is not on PATH in this shell. Add it to PATH, then retry." + "value": "One or more surfaces have a running process; retry with force=true" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "restore: このセッションは「%1$@ claude proxy」で起動されましたが、このシェルの PATH に「%1$@」がありません。PATH に追加してから、再試行してください。" + "value": "سطح واحد أو أكثر يحتوي على عملية قيد التشغيل؛ أعد المحاولة باستخدام force=true" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "restore:此会话是通过“%1$@ claude proxy”启动的,但此 shell 的 PATH 中没有“%1$@”。请将其添加到 PATH,然后重试。" + "value": "Mindestens eine Oberfläche hat einen laufenden Prozess; erneut mit force=true versuchen" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "restore: Diese Sitzung wurde mit „%1$@ claude proxy“ gestartet, aber „%1$@“ ist in dieser Shell nicht im PATH. Fügen Sie es zum PATH hinzu und versuchen Sie es dann erneut." + "value": "Una o más superficies tienen un proceso en ejecución; vuelve a intentarlo con force=true" } }, "fr": { "stringUnit": { "state": "translated", - "value": "restore : cette session a été démarrée avec « %1$@ claude proxy », mais « %1$@ » n'est pas dans le PATH de ce shell. Ajoutez-le au PATH, puis réessayez." + "value": "Une ou plusieurs surfaces ont un processus en cours d’exécution ; réessayez avec force=true" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "restore: بدأت هذه الجلسة باستخدام '%1$@ claude proxy'، لكن '%1$@' غير موجود في PATH في هذه الصدفة. أضفه إلى PATH، ثم أعد المحاولة." + "value": "1 つ以上のサーフェスでプロセスが実行中です。force=true を指定して再試行してください" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "restore: esta sesión se inició con '%1$@ claude proxy', pero '%1$@' no está en el PATH de este shell. Agréguelo al PATH y vuelva a intentarlo." + "value": "하나 이상의 표면에서 프로세스가 실행 중입니다. force=true로 다시 시도하세요" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "restore:此工作階段是以「%1$@ claude proxy」啟動的,但此 shell 的 PATH 中沒有「%1$@」。請將其加入 PATH,然後重試。" + "value": "一个或多个表面有正在运行的进程;请使用 force=true 重试" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "restore: 이 세션은 '%1$@ claude proxy'로 시작되었지만 이 셸의 PATH에 '%1$@'이(가) 없습니다. PATH에 추가한 후 다시 시도하십시오." + "value": "一個或多個表面有正在執行的程序;請使用 force=true 重試" } } } }, - "settings.terminal.textEditingCommandMovesByWord": { + "cli.socket.error.workspaceCloseConfirmationRequired": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Command Moves by Word" + "value": "Workspace has a running process; retry with --force" } }, "ar": { "stringUnit": { "state": "translated", - "value": "Command ينقل حسب الكلمة" + "value": "تحتوي مساحة العمل على عملية قيد التشغيل؛ أعد المحاولة باستخدام --force" } }, "de": { "stringUnit": { "state": "translated", - "value": "Befehlstaste bewegt wortweise" + "value": "Arbeitsbereich mit laufendem Prozess; erneut mit --force versuchen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Comando mueve por palabra" + "value": "El espacio de trabajo tiene un proceso en ejecución; vuelve a intentarlo con --force" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Commande déplace par mot" + "value": "L’espace de travail a un processus en cours d’exécution ; réessayez avec --force" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Command で単語単位に移動" + "value": "ワークスペースでプロセスが実行中です。--force を指定して再試行してください" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Command로 단어 단위 이동" + "value": "워크스페이스에서 프로세스가 실행 중입니다. --force로 다시 시도하세요" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Command 按词移动" + "value": "此工作区有正在运行的进程;请使用 --force 重试" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Command 依字移動" + "value": "此工作區有正在執行的程序;請使用 --force 重試" } } } }, - "settings.terminal.textEditingCommandMovesByWord.subtitleOn": { + "cli.socket.error.windowCloseConfirmationRequired": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Command arrow and delete keys move and delete by word, like Option. Control Left and Right Arrow move to the start and end of the line." + "value": "One or more workspaces or Dock surfaces have a running process; retry with --force" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تنقل مفاتيح الأسهم والحذف مع Command وتحذف حسب الكلمة، مثل Option. وينقل Control مع السهمين الأيسر والأيمن إلى بداية السطر ونهايته." + "value": "تحتوي مساحة عمل واحدة أو أكثر أو أسطح Dock على عملية قيد التشغيل؛ أعد المحاولة باستخدام --force" } }, "de": { "stringUnit": { "state": "translated", - "value": "Befehlstaste mit Pfeil- und Löschtasten bewegt und löscht wortweise, wie die Wahltaste. Control mit Links- und Rechtspfeil springt an Anfang und Ende der Zeile." + "value": "Mindestens ein Workspace oder eine Dock-Oberfläche hat einen laufenden Prozess; erneut mit --force versuchen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Las flechas y la tecla Eliminar con Comando mueven y borran por palabra, como Opción. Control con las flechas izquierda y derecha va al inicio y al final de la línea." + "value": "Uno o más espacios de trabajo o superficies del Dock tienen un proceso en ejecución; vuelve a intentarlo con --force" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les flèches et la touche Supprimer avec Commande déplacent et suppriment par mot, comme Option. Contrôle avec les flèches gauche et droite va au début et à la fin de la ligne." + "value": "Un ou plusieurs espaces de travail ou surfaces du Dock ont un processus en cours d’exécution ; réessayez avec --force" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Command と矢印キー・削除キーで、Option と同じく単語単位に移動・削除します。Control と左右矢印キーで行頭・行末に移動します。" + "value": "1 つ以上のワークスペースまたは Dock サーフェスでプロセスが実行中です。--force を指定して再試行してください" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Command 화살표 및 삭제 키가 Option처럼 단어 단위로 이동하고 삭제합니다. Control 왼쪽 및 오른쪽 화살표는 줄의 시작과 끝으로 이동합니다." + "value": "하나 이상의 워크스페이스 또는 Dock 표면에서 실행 중인 프로세스가 있습니다. --force로 다시 시도하세요" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Command 加方向键和删除键按词移动和删除,与 Option 相同。Control 加左右方向键移到行首和行尾。" + "value": "一个或多个工作区或 Dock 表面有正在运行的进程;请使用 --force 重试" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Command 加方向鍵與刪除鍵會依字移動與刪除,與 Option 相同。Control 加左右方向鍵會移到行首與行尾。" + "value": "一個或多個工作區或 Dock 表面有正在執行的程序;請使用 --force 重試" } } } }, - "settings.terminal.textEditingCommandMovesByWord.subtitleOff": { + "command.copyWorkingDirectory.title": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "Command arrow and delete keys move and delete by line, as in macOS text fields." + "value": "نسخ دليل العمل" } }, - "ar": { + "bs": { "stringUnit": { "state": "translated", - "value": "تنقل مفاتيح الأسهم والحذف مع Command وتحذف حسب السطر، كما في حقول النص في macOS." + "value": "Kopiraj radni direktorij" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kopiér arbejdsmappe" } }, "de": { "stringUnit": { "state": "translated", - "value": "Befehlstaste mit Pfeil- und Löschtasten bewegt und löscht zeilenweise, wie in macOS-Textfeldern." + "value": "Arbeitsverzeichnis kopieren" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy Working Directory" } }, "es": { "stringUnit": { "state": "translated", - "value": "Las flechas y la tecla Eliminar con Comando mueven y borran por línea, como en los campos de texto de macOS." + "value": "Copiar directorio de trabajo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les flèches et la touche Supprimer avec Commande déplacent et suppriment par ligne, comme dans les champs de texte de macOS." + "value": "Copier le répertoire de travail" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia directory di lavoro" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Command と矢印キー・削除キーで、macOS のテキストフィールドと同じく行単位に移動・削除します。" + "value": "作業ディレクトリをコピー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Command 화살표 및 삭제 키가 macOS 텍스트 필드처럼 줄 단위로 이동하고 삭제합니다." + "value": "작업 디렉터리 복사" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kopier arbeidsmappe" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kopiuj katalog roboczy" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar Diretório de Trabalho" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Копировать рабочий каталог" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คัดลอกไดเรกทอรีทำงาน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Çalışma Dizinini Kopyala" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Копіювати робочий каталог" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Command 加方向键和删除键按行移动和删除,与 macOS 文本框相同。" + "value": "复制工作目录" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Command 加方向鍵與刪除鍵會依行移動與刪除,與 macOS 文字欄位相同。" + "value": "複製工作目錄" } } } }, - "settings.terminal.textEditingGesturesInFullScreenApps": { + "command.copyProjectRoot.title": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "Text Editing Gestures in Full-Screen Apps" + "value": "نسخ جذر المشروع" } }, - "ar": { + "bs": { "stringUnit": { "state": "translated", - "value": "إيماءات تحرير النص في التطبيقات بملء الشاشة" + "value": "Kopiraj korijen projekta" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kopiér projektrod" } }, "de": { "stringUnit": { "state": "translated", - "value": "Textbearbeitungsgesten in Vollbild-Apps" + "value": "Projektstamm kopieren" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy Project Root" } }, "es": { "stringUnit": { "state": "translated", - "value": "Gestos de edición de texto en apps a pantalla completa" + "value": "Copiar raíz del proyecto" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Gestes d’édition de texte dans les apps plein écran" + "value": "Copier la racine du projet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia radice del progetto" } }, "ja": { "stringUnit": { "state": "translated", - "value": "フルスクリーンアプリでのテキスト編集ジェスチャー" + "value": "プロジェクトのルートをコピー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "전체 화면 앱에서 텍스트 편집 제스처" + "value": "프로젝트 루트 복사" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kopier prosjektrot" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kopiuj katalog główny projektu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar Raiz do Projeto" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Копировать корень проекта" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คัดลอกรากโปรเจกต์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Proje Kökünü Kopyala" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Копіювати корінь проєкту" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "全屏应用中的文本编辑手势" + "value": "复制项目根目录" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "全螢幕 App 中的文字編輯手勢" + "value": "複製專案根目錄" } } } }, - "settings.terminal.textEditingGesturesInFullScreenApps.subtitle": { + "command.copyScreen.title": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "Gestures also apply while a full-screen app such as vim, less, or tmux is running, so they keep working at a shell inside tmux, screen, or zellij." + "value": "نسخ الشاشة المرئية" } }, - "ar": { + "bs": { "stringUnit": { "state": "translated", - "value": "تنطبق الإيماءات أيضًا أثناء تشغيل تطبيق بملء الشاشة مثل vim أو less أو tmux، فتظل تعمل في الصدفة داخل tmux أو screen أو zellij." + "value": "Kopiraj vidljivi ekran" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Kopiér synlig skærm" } }, "de": { "stringUnit": { "state": "translated", - "value": "Gesten gelten auch, während eine Vollbild-App wie vim, less oder tmux läuft, und funktionieren so weiter an einer Shell in tmux, screen oder zellij." + "value": "Sichtbaren Bildschirm kopieren" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy Visible Screen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Los gestos también se aplican mientras se ejecuta una app a pantalla completa como vim, less o tmux, así que siguen funcionando en una shell dentro de tmux, screen o zellij." + "value": "Copiar pantalla visible" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les gestes s’appliquent aussi lorsqu’une app plein écran comme vim, less ou tmux est en cours d’exécution, et continuent donc de fonctionner dans un shell sous tmux, screen ou zellij." + "value": "Copier l'écran visible" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia schermo visibile" } }, "ja": { "stringUnit": { "state": "translated", - "value": "vim、less、tmux などのフルスクリーンアプリの実行中もジェスチャーが有効になるため、tmux、screen、zellij 内のシェルでも引き続き使えます。" + "value": "表示中の画面をコピー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "vim, less, tmux 같은 전체 화면 앱이 실행 중일 때도 제스처가 적용되므로 tmux, screen, zellij 안의 셸에서도 계속 작동합니다." + "value": "보이는 화면 복사" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Kopier synlig skjerm" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kopiuj widoczny ekran" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar Tela Visível" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Копировать видимый экран" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คัดลอกหน้าจอที่มองเห็น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Görünür Ekranı Kopyala" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Копіювати видимий екран" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在 vim、less 或 tmux 等全屏应用运行时手势也会生效,因此在 tmux、screen 或 zellij 中的 shell 里仍可使用。" + "value": "复制可见屏幕" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在 vim、less 或 tmux 等全螢幕 App 執行時手勢也會生效,因此在 tmux、screen 或 zellij 中的 shell 裡仍可使用。" + "value": "複製可見畫面" } } } @@ -589893,776 +592115,6 @@ } } }, - "command.copyWorkingDirectory.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ دليل العمل" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Kopiraj radni direktorij" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopiér arbejdsmappe" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Arbeitsverzeichnis kopieren" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Copy Working Directory" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar directorio de trabajo" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copier le répertoire de travail" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copia directory di lavoro" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "作業ディレクトリをコピー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "작업 디렉터리 복사" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopier arbeidsmappe" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kopiuj katalog roboczy" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiar Diretório de Trabalho" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Копировать рабочий каталог" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอกไดเรกทอรีทำงาน" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Çalışma Dizinini Kopyala" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Копіювати робочий каталог" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制工作目录" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "複製工作目錄" - } - } - } - }, - "command.copyProjectRoot.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ جذر المشروع" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Kopiraj korijen projekta" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopiér projektrod" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Projektstamm kopieren" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Copy Project Root" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar raíz del proyecto" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copier la racine du projet" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copia radice del progetto" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "プロジェクトのルートをコピー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "프로젝트 루트 복사" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopier prosjektrot" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kopiuj katalog główny projektu" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiar Raiz do Projeto" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Копировать корень проекта" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอกรากโปรเจกต์" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Proje Kökünü Kopyala" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Копіювати корінь проєкту" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制项目根目录" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "複製專案根目錄" - } - } - } - }, - "command.copyScreen.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ الشاشة المرئية" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Kopiraj vidljivi ekran" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Kopiér synlig skærm" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Sichtbaren Bildschirm kopieren" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Copy Visible Screen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar pantalla visible" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Copier l'écran visible" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Copia schermo visibile" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "表示中の画面をコピー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "보이는 화면 복사" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Kopier synlig skjerm" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kopiuj widoczny ekran" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Copiar Tela Visível" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Копировать видимый экран" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คัดลอกหน้าจอที่มองเห็น" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Görünür Ekranı Kopyala" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Копіювати видимий екран" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制可见屏幕" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "複製可見畫面" - } - } - } - }, - "socket.sidebar.closeSurface.confirmationRequired": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Surface has a running process; retry with --force" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تحتوي السطح على عملية قيد التشغيل؛ أعد المحاولة باستخدام --force" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Oberfläche hat einen laufenden Prozess; erneut mit --force versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "La superficie tiene un proceso en ejecución; vuelve a intentarlo con --force" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "La surface a un processus en cours d’exécution ; réessayez avec --force" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サーフェスでプロセスが実行中です。--force を指定して再試行してください" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "표면에서 실행 중인 프로세스가 있습니다. --force로 다시 시도하세요" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此表面有正在运行的进程;请使用 --force 重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此表面有正在執行的程序;請使用 --force 重試" - } - } - } - }, - "socket.sidebar.closeSurface.failed": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Failed to close surface" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذر إغلاق السطح" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Oberfläche konnte nicht geschlossen werden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se pudo cerrar la superficie" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Échec de la fermeture de la surface" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サーフェスを閉じられませんでした" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "표면을 닫지 못했습니다" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法关闭此表面" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "無法關閉表面" - } - } - } - }, - "socket.surface.close.confirmationRequired": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Surface has a running process; retry with force=true" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تحتوي السطح على عملية قيد التشغيل؛ أعد المحاولة باستخدام force=true" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Oberfläche hat einen laufenden Prozess; erneut mit force=true versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "La superficie tiene un proceso en ejecución; vuelve a intentarlo con force=true" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "La surface a un processus en cours d’exécution ; réessayez avec force=true" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サーフェスでプロセスが実行中です。force=true を指定して再試行してください" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "표면에서 실행 중인 프로세스가 있습니다. force=true로 다시 시도하세요" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此表面有正在运行的进程;请使用 force=true 重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此表面有正在執行的程序;請使用 force=true 重試" - } - } - } - }, - "socket.surface.close.failed": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Failed to close surface" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تعذر إغلاق السطح" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Oberfläche konnte nicht geschlossen werden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se pudo cerrar la superficie" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Échec de la fermeture de la surface" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サーフェスを閉じられませんでした" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "표면을 닫지 못했습니다" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法关闭此表面" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "無法關閉表面" - } - } - } - }, - "socket.tabAction.close.confirmationRequired": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "One or more surfaces have a running process; retry with force=true" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "سطح واحد أو أكثر يحتوي على عملية قيد التشغيل؛ أعد المحاولة باستخدام force=true" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Mindestens eine Oberfläche hat einen laufenden Prozess; erneut mit force=true versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Una o más superficies tienen un proceso en ejecución; vuelve a intentarlo con force=true" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Une ou plusieurs surfaces ont un processus en cours d’exécution ; réessayez avec force=true" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "1 つ以上のサーフェスでプロセスが実行中です。force=true を指定して再試行してください" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "하나 이상의 표면에서 프로세스가 실행 중입니다. force=true로 다시 시도하세요" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "一个或多个表面有正在运行的进程;请使用 force=true 重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "一個或多個表面有正在執行的程序;請使用 force=true 重試" - } - } - } - }, - "cli.socket.error.workspaceCloseConfirmationRequired": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Workspace has a running process; retry with --force" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تحتوي مساحة العمل على عملية قيد التشغيل؛ أعد المحاولة باستخدام --force" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Arbeitsbereich mit laufendem Prozess; erneut mit --force versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "El espacio de trabajo tiene un proceso en ejecución; vuelve a intentarlo con --force" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "L’espace de travail a un processus en cours d’exécution ; réessayez avec --force" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ワークスペースでプロセスが実行中です。--force を指定して再試行してください" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "워크스페이스에서 프로세스가 실행 중입니다. --force로 다시 시도하세요" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此工作区有正在运行的进程;请使用 --force 重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此工作區有正在執行的程序;請使用 --force 重試" - } - } - } - }, - "cli.socket.error.windowCloseConfirmationRequired": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "One or more workspaces or Dock surfaces have a running process; retry with --force" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تحتوي مساحة عمل واحدة أو أكثر أو أسطح Dock على عملية قيد التشغيل؛ أعد المحاولة باستخدام --force" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Mindestens ein Workspace oder eine Dock-Oberfläche hat einen laufenden Prozess; erneut mit --force versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Uno o más espacios de trabajo o superficies del Dock tienen un proceso en ejecución; vuelve a intentarlo con --force" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Un ou plusieurs espaces de travail ou surfaces du Dock ont un processus en cours d’exécution ; réessayez avec --force" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "1 つ以上のワークスペースまたは Dock サーフェスでプロセスが実行中です。--force を指定して再試行してください" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "하나 이상의 워크스페이스 또는 Dock 표면에서 실행 중인 프로세스가 있습니다. --force로 다시 시도하세요" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "一个或多个工作区或 Dock 表面有正在运行的进程;请使用 --force 重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "一個或多個工作區或 Dock 表面有正在執行的程序;請使用 --force 重試" - } - } - } - }, "agent.generic.status.runningSubagents": { "extractionState": "manual", "localizations": { @@ -591430,2284 +592882,1005 @@ } } }, - "cli.help.record": { + "machines.meter.upgrade": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nRecord a cmux window to an mp4 or gif, for a pull request or a bug\nreport. Only cmux's own windows can be recorded, and no Screen\nRecording permission is involved.\n\nFlags for start:\n --format mp4|gif Output format (default: mp4)\n --gif Shorthand for --format gif\n --fps <1-30> Frames per second (default: 12 for mp4, 8 for gif)\n --max-seconds Stop by itself after n seconds, 0.5-120 (default: 15)\n --scale <0.1-1> Scale the output (default: 1 for mp4, 0.5 for gif)\n --max-width <64-4096> Cap the output width, keeping the aspect ratio\n --region Record part of the window, in window points\n --out Write here instead of a temporary directory\n --label Name the file\n --no-captions Do not draw `record note` captions into the clip\n --window Window to record (default: the frontmost one)\n\nOutput: ` `, or the full response with --json.\n\nA recording stops itself at --max-seconds, so a clip is never left\nrunning by an agent that goes away. One recording at a time.\n\nExample:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "Upgrade for more" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nتسجيل نافذة cmux بصيغة mp4 أو gif، من أجل طلب سحب أو تقرير خطأ.\nلا يمكن تسجيل سوى نوافذ cmux نفسها،\nولا يتطلب ذلك إذن تسجيل الشاشة.\n\nخيارات start:\n --format mp4|gif صيغة الإخراج (الافتراضي: mp4)\n --gif اختصار لـ --format gif\n --fps <1-30> الإطارات في الثانية (الافتراضي: 12 لـ mp4، 8 لـ gif)\n --max-seconds يتوقف من تلقاء نفسه بعد n ثانية، 0.5-120 (الافتراضي: 15)\n --scale <0.1-1> تحجيم الإخراج (الافتراضي: 1 لـ mp4، 0.5 لـ gif)\n --max-width <64-4096> تحديد أقصى عرض للإخراج مع الحفاظ على نسبة الأبعاد\n --region تسجيل جزء من النافذة، بوحدات نقاط النافذة\n --out الكتابة هنا بدلاً من دليل مؤقت\n --label تسمية الملف\n --no-captions عدم رسم تعليقات `record note` داخل المقطع\n --window النافذة المراد تسجيلها (الافتراضي: النافذة الأمامية)\n\nالإخراج: ` `، أو الاستجابة الكاملة مع --json.\n\nيتوقف التسجيل من تلقاء نفسه عند --max-seconds، حتى لا يترك وكيل غادر\nمقطعًا قيد التسجيل. تسجيل واحد في كل مرة.\n\nمثال:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "アップグレードで追加" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux record [start] [Flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nEin cmux-Fenster als mp4 oder gif aufzeichnen, für einen Pull Request\noder einen Fehlerbericht. Nur cmux-eigene Fenster können aufgezeichnet\nwerden, und es ist keine Bildschirmaufnahme-Berechtigung beteiligt.\n\nFlags für start:\n --format mp4|gif Ausgabeformat (Standard: mp4)\n --gif Kurzform für --format gif\n --fps <1-30> Bilder pro Sekunde (Standard: 12 für mp4, 8 für gif)\n --max-seconds Nach n Sekunden selbst beenden, 0.5-120 (Standard: 15)\n --scale <0.1-1> Ausgabe skalieren (Standard: 1 für mp4, 0.5 für gif)\n --max-width <64-4096> Ausgabebreite begrenzen, Seitenverhältnis bleibt\n --region Teil des Fensters aufzeichnen, in Fensterpunkten\n --out Hierhin schreiben statt in ein temporäres Verzeichnis\n --label Datei benennen\n --no-captions Keine `record note`-Untertitel in den Clip zeichnen\n --window Fenster zum Aufzeichnen (Standard: das vorderste)\n\nAusgabe: ` `, oder mit --json die vollständige Antwort.\n\nEine Aufzeichnung beendet sich bei --max-seconds selbst, damit ein Agent, der\nverschwindet, keinen laufenden Clip hinterlässt. Eine Aufzeichnung zur Zeit.\n\nBeispiel:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "升级以获得更多" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Uso: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nGraba una ventana de cmux en mp4 o gif, para un pull request o un informe\nde error. Solo se pueden grabar las ventanas propias de cmux, y no\ninterviene ningún permiso de grabación de pantalla.\n\nFlags para start:\n --format mp4|gif Formato de salida (predeterminado: mp4)\n --gif Atajo para --format gif\n --fps <1-30> Fotogramas por segundo (predeterminado: 12 para mp4, 8 para gif)\n --max-seconds Se detiene solo tras n segundos, 0.5-120 (predeterminado: 15)\n --scale <0.1-1> Escala la salida (predeterminado: 1 para mp4, 0.5 para gif)\n --max-width <64-4096> Limita el ancho de salida manteniendo la proporción\n --region Graba parte de la ventana, en puntos de ventana\n --out Escribe aquí en lugar de en un directorio temporal\n --label Da nombre al archivo\n --no-captions No dibujar los subtítulos de `record note` en el clip\n --window Ventana que se graba (predeterminado: la del frente)\n\nSalida: ` `, o la respuesta completa con --json.\n\nUna grabación se detiene sola en --max-seconds, para que un agente que\ndesaparece nunca deje un clip en marcha. Una grabación a la vez.\n\nEjemplo:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "Upgrade für mehr" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nEnregistrer une fenêtre cmux en mp4 ou gif, pour une pull request ou un\nrapport de bug. Seules les fenêtres de cmux peuvent être enregistrées, et\naucune autorisation d'enregistrement de l'écran n'intervient.\n\nFlags pour start :\n --format mp4|gif Format de sortie (par défaut : mp4)\n --gif Raccourci pour --format gif\n --fps <1-30> Images par seconde (par défaut : 12 pour mp4, 8 pour gif)\n --max-seconds S'arrête seul après n secondes, 0.5-120 (par défaut : 15)\n --scale <0.1-1> Mettre la sortie à l'échelle (par défaut : 1 pour mp4, 0.5 pour gif)\n --max-width <64-4096> Limiter la largeur de sortie en gardant les proportions\n --region N'enregistrer qu'une partie de la fenêtre, en points fenêtre\n --out Écrire ici au lieu d'un répertoire temporaire\n --label Nommer le fichier\n --no-captions Ne pas dessiner les légendes `record note` dans le clip\n --window Fenêtre à enregistrer (par défaut : celle au premier plan)\n\nSortie : ` `, ou la réponse complète avec --json.\n\nUn enregistrement s'arrête de lui-même à --max-seconds, afin qu'un agent qui\ndisparaît ne laisse jamais un clip en cours. Un seul enregistrement à la fois.\n\nExemple :\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "Passez au forfait supérieur pour en obtenir plus" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "使用方法: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\nプルリクエストやバグ報告のために、cmux のウィンドウを mp4 または gif\nで録画します。録画できるのは cmux 自身のウィンドウだけで、画面収録の\n権限は必要ありません。\n\nstart のフラグ:\n --format mp4|gif 出力形式(既定: mp4)\n --gif --format gif の短縮形\n --fps <1-30> 1 秒あたりのフレーム数(既定: mp4 は 12、gif は 8)\n --max-seconds n 秒後に自動停止、0.5-120(既定: 15)\n --scale <0.1-1> 出力を拡大縮小(既定: mp4 は 1、gif は 0.5)\n --max-width <64-4096> 縦横比を保ったまま出力の幅を制限\n --region ウィンドウの一部だけを録画(ウィンドウポイント単位)\n --out 一時ディレクトリではなくこのパスに書き出す\n --label ファイルに名前を付ける\n --no-captions `record note` のキャプションをクリップに描画しない\n --window 録画するウィンドウ(既定: 最前面のウィンドウ)\n\n出力: ` `、または --json で完全なレスポンス。\n\n録画は --max-seconds で自動停止するので、いなくなったエージェントが\n録画を実行したままにすることはありません。録画は同時に 1 つだけです。\n\n例:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "ترقية للحصول على المزيد" } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "사용법: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\n풀 리퀘스트나 버그 리포트에 쓰려고 cmux 창을 mp4 또는 gif로\n녹화합니다. cmux 자신의 창만 녹화할 수 있고, 화면 기록 권한은\n필요하지 않습니다.\n\nstart 플래그:\n --format mp4|gif 출력 형식 (기본값: mp4)\n --gif --format gif의 단축형\n --fps <1-30> 초당 프레임 수 (기본값: mp4는 12, gif는 8)\n --max-seconds n초 후 스스로 정지, 0.5-120 (기본값: 15)\n --scale <0.1-1> 출력 크기 조정 (기본값: mp4는 1, gif는 0.5)\n --max-width <64-4096> 가로 세로 비율을 유지하며 출력 너비 제한\n --region 창의 일부만 녹화, 창 포인트 단위\n --out 임시 디렉터리 대신 이 경로에 기록\n --label 파일 이름 지정\n --no-captions `record note` 캡션을 클립에 그리지 않음\n --window 녹화할 창 (기본값: 맨 앞 창)\n\n출력: ` `, 또는 --json으로 전체 응답.\n\n녹화는 --max-seconds에서 스스로 멈추므로 사라진 에이전트가 녹화를 계속\n남겨 두는 일은 없습니다. 한 번에 하나의 녹화만 가능합니다.\n\n예:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "Actualizar para obtener más" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "使用方法: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\n将 cmux 窗口录制为 mp4 或 gif,用于 pull request 或错误报告。\n只能录制 cmux 自己的窗口,\n而且不需要任何屏幕录制权限。\n\nstart 的标志:\n --format mp4|gif 输出格式(默认: mp4)\n --gif --format gif 的简写\n --fps <1-30> 每秒帧数(默认: mp4 为 12,gif 为 8)\n --max-seconds n 秒后自行停止,0.5-120(默认: 15)\n --scale <0.1-1> 缩放输出(默认: mp4 为 1,gif 为 0.5)\n --max-width <64-4096> 限制输出宽度并保持宽高比\n --region 只录制窗口的一部分,以窗口点为单位\n --out 写入此处而非临时目录\n --label 为文件命名\n --no-captions 不要把 `record note` 字幕画进视频\n --window 要录制的窗口(默认: 最前面的窗口)\n\n输出: ` `,或用 --json 获取完整响应。\n\n录制会在 --max-seconds 时自行停止,因此离开的代理不会留下仍在录制的\n视频。同一时间只能有一份录制。\n\n示例:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "升級以取得更多" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "使用方法: cmux record [start] [flags]\n cmux record stop|status [--id ]\n cmux record note [--id ] \n cmux record list\n\n將 cmux 視窗錄製成 mp4 或 gif,用於 pull request 或錯誤報告。\n只能錄製 cmux 自己的視窗,\n而且不需要任何螢幕錄製權限。\n\nstart 的旗標:\n --format mp4|gif 輸出格式(預設: mp4)\n --gif --format gif 的簡寫\n --fps <1-30> 每秒影格數(預設: mp4 為 12,gif 為 8)\n --max-seconds n 秒後自行停止,0.5-120(預設: 15)\n --scale <0.1-1> 縮放輸出(預設: mp4 為 1,gif 為 0.5)\n --max-width <64-4096> 限制輸出寬度並維持長寬比\n --region 只錄製視窗的一部分,以視窗點為單位\n --out 寫入此處而非暫存目錄\n --label 為檔案命名\n --no-captions 不要將 `record note` 字幕畫進影片\n --window 要錄製的視窗(預設: 最前面的視窗)\n\n輸出: ` `,或以 --json 取得完整回應。\n\n錄製會在 --max-seconds 時自行停止,因此離開的代理程式不會留下仍在\n錄製的影片。同一時間只能有一份錄製。\n\n範例:\n cmux record start --gif --max-seconds 8 --label sidebar-drag\n cmux record note \"dragging the workspace\"\n cmux record stop" + "value": "더 사용하려면 업그레이드" } } } }, - "cli.record.error.formatConflict": { + "settings.betaFeatures.dock.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "record start: --gif conflicts with --format" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "record start: لا يمكن دمج --gif مع --format" + "value": "Adds Dock to the right sidebar for custom terminal controls." } }, "de": { "stringUnit": { "state": "translated", - "value": "record start: --gif kann nicht mit --format kombiniert werden" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "record start: --gif no se puede combinar con --format" + "value": "Fügt der rechten Seitenleiste das Dock für benutzerdefinierte Terminal-Steuerelemente hinzu." } }, "fr": { "stringUnit": { "state": "translated", - "value": "record start: --gif ne peut pas être combiné avec --format" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "record start: --gif は --format と併用できません" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "record start: --gif은 --format과 함께 사용할 수 없습니다" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "record start: --gif 不能与 --format 同用" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "record start: --gif 無法與 --format 同時使用" - } - } - } - }, - "cli.record.error.noteText": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "record note: text is required" + "value": "Ajoute le Dock à la barre latérale droite pour les commandes de terminal personnalisées." } }, "ar": { "stringUnit": { "state": "translated", - "value": "record note: النص مطلوب" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "record note: Text ist erforderlich" + "value": "يضيف Dock إلى الشريط الجانبي الأيمن لعناصر التحكم المخصصة في الطرفية." } }, "es": { "stringUnit": { "state": "translated", - "value": "record note: se requiere el texto" + "value": "Añade el Dock a la barra lateral derecha para controles de terminal personalizados." } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "record note : le texte est requis" + "value": "在右側邊欄加入 Dock,用來放置自訂的終端機控制項。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "record note: テキストが必要です" + "value": "在右侧边栏添加 Dock,用来放置自定义的终端控件。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "record note: 텍스트가 필요합니다" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "record note: 必须提供文本" + "value": "오른쪽 사이드바에 Dock을 추가해 터미널 컨트롤을 직접 구성할 수 있습니다." } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "record note: 必須提供文字" + "value": "右サイドバーに Dock を追加し、ターミナル用のカスタムコントロールを配置できるようにします。" } } } }, - "cli.record.error.unexpectedArguments": { + "cli.agentMessage.noAgent": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "record %@: unexpected arguments: %@" + "value": "No agent activity seen on that surface yet. The message waits until an agent there picks it up." } }, "ar": { "stringUnit": { "state": "translated", - "value": "record %@: وسائط غير متوقعة: %@" + "value": "لم يُرصد أي نشاط لوكيل على هذا السطح بعد. تنتظر الرسالة حتى يستلمها وكيل هناك." } }, "de": { "stringUnit": { "state": "translated", - "value": "record %@: unerwartete Argumente: %@" + "value": "Auf dieser Oberfläche wurde noch keine Agent-Aktivität erkannt. Die Nachricht wartet, bis ein Agent dort sie abholt." } }, "es": { "stringUnit": { "state": "translated", - "value": "record %@: argumentos inesperados: %@" + "value": "Todavía no se ha visto actividad de ningún agente en esa superficie. El mensaje espera hasta que un agente allí lo recoja." } }, "fr": { "stringUnit": { "state": "translated", - "value": "record %@ : arguments inattendus : %@" + "value": "Aucune activité d'agent détectée sur cette surface pour l'instant. Le message attend qu'un agent l'y récupère." } }, "ja": { "stringUnit": { "state": "translated", - "value": "record %@: 予期しない引数: %@" + "value": "そのサーフェスではまだエージェントの動きが確認されていません。そこのエージェントが受け取るまでメッセージは待機します。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "record %@: 예기치 않은 인수: %@" + "value": "해당 표면에서 아직 에이전트 활동이 없습니다. 그곳의 에이전트가 가져갈 때까지 메시지가 대기합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "record %@: 多余的参数: %@" + "value": "尚未在该表面看到代理活动。消息会等待,直到那里的代理取走它。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "record %@: 未預期的引數: %@" + "value": "尚未在該表面看到代理活動。訊息會等待,直到那裡的代理取走它。" } } } }, - "cli.record.error.unknownSubcommand": { + "cli.agentMessage.queued": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "record: unknown subcommand '%@' (start, stop, status, note, list)" + "value": "Queued message %@ for %@ (%@)." } }, "ar": { "stringUnit": { "state": "translated", - "value": "record: أمر فرعي غير معروف '%@' (start، stop، status، note، list)" + "value": "وُضعت الرسالة %1$@ في قائمة الانتظار لـ %2$@ (%3$@)." } }, "de": { "stringUnit": { "state": "translated", - "value": "record: unbekannter Unterbefehl „%@“ (start, stop, status, note, list)" + "value": "Nachricht %1$@ für %2$@ (%3$@) in die Warteschlange gestellt." } }, "es": { "stringUnit": { "state": "translated", - "value": "record: subcomando desconocido '%@' (start, stop, status, note, list)" + "value": "Mensaje %1$@ en cola para %2$@ (%3$@)." } }, "fr": { "stringUnit": { "state": "translated", - "value": "record : sous-commande inconnue '%@' (start, stop, status, note, list)" + "value": "Message %1$@ mis en file pour %2$@ (%3$@)." } }, "ja": { "stringUnit": { "state": "translated", - "value": "record: 不明なサブコマンド '%@' です(start、stop、status、note、list)" + "value": "メッセージ %1$@ を %2$@(%3$@)宛てにキューに入れました。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "record: 알 수 없는 하위 명령 '%@' (start, stop, status, note, list)" + "value": "메시지 %1$@을(를) %2$@(%3$@) 앞으로 대기열에 넣었습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "record: 未知的子命令 '%@'(start、stop、status、note、list)" + "value": "已将消息 %1$@ 排入 %2$@(%3$@)的队列。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "record: 未知的子命令 '%@'(start、stop、status、note、list)" + "value": "已將訊息 %1$@ 排入 %2$@(%3$@)的佇列。" } } } }, - "cli.usage.record": { + "cli.help.agentInbox": { "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "الاستخدام: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nيعرض رسائل الوكلاء، الأحدث أولًا. بدون --surface، يعرض الرسائل لكل\nالأسطح. يضع --mark-read علامة مقروءة على الرسائل المعروضة." } }, "de": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "Verwendung: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nListet Agent-Nachrichten auf, neueste zuerst. Ohne --surface werden Nachrichten für\nalle Oberflächen aufgelistet. --mark-read markiert die aufgelisteten Nachrichten als gelesen." } }, "en": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "Usage: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nList agent messages, newest first. Without --surface, lists messages for\nevery surface. --mark-read marks the listed messages read." } }, "es": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "Uso: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nLista los mensajes de agentes, del más reciente al más antiguo. Sin --surface, lista los mensajes de\ntodas las superficies. --mark-read marca como leídos los mensajes listados." } }, "fr": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "Utilisation : cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nListe les messages d'agent, du plus récent au plus ancien. Sans --surface, liste les messages de\ntoutes les surfaces. --mark-read marque les messages listés comme lus." } }, "ja": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "使い方: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nエージェントのメッセージを新しい順に一覧表示します。--surface を指定しないと\nすべてのサーフェスのメッセージを表示します。--mark-read は表示したメッセージを既読にします。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "사용법: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\n에이전트 메시지를 최신순으로 나열합니다. --surface가 없으면\n모든 표면의 메시지를 나열합니다. --mark-read는 나열된 메시지를 읽음으로 표시합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "用法: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\n列出代理消息,最新的在前。未指定 --surface 时,列出\n所有表面的消息。--mark-read 会将列出的消息标记为已读。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "record [start] [--format mp4|gif] [--gif] [--fps ] [--max-seconds ] [--scale ] [--max-width ] [--region ] [--out ] [--label ] [--no-captions] [--window ] | record stop|status [--id ] | record note [--id ] | record list" + "value": "用法: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\n列出代理訊息,最新的在前。未指定 --surface 時,列出\n所有表面的訊息。--mark-read 會將列出的訊息標為已讀。" } } } }, - "cli.help.shot": { + "cli.help.agentMessage": { "extractionState": "manual", "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Usage: cmux shot [flags]\n\nScreenshot a cmux window, or a region of one, to a png or a jpeg. Only\ncmux's own windows can be captured, and no Screen Recording permission\nis involved, so this works on a release build and inside CI.\n\nFlags:\n --format png|jpg Output format (default: png)\n --jpg Shorthand for --format jpg\n --scale <0.1-1> Scale the output (default: 1)\n --max-width <64-8192> Cap the output width, keeping the aspect ratio\n --quality <0.1-1> JPEG quality (default: 0.8, ignored for png)\n --region Capture part of the window, in window points\n --out Write here instead of a temporary directory\n --label Name the file\n --caption Draw a caption into the image\n --window Window to capture (default: the frontmost one)\n\nOutput: `x `, or the full response with --json.\n\nA region is the same rectangle `cmux record --region` films, so a\ndetail spotted in a clip can be shot with the same four numbers.\n\nExample:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - }, "ar": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux shot [flags]\n\nتصوير نافذة cmux أو جزء منها بصيغة png أو jpeg.\nلا يمكن تصوير سوى نوافذ cmux نفسها، ولا يتطلب ذلك إذن تسجيل الشاشة،\nلذلك يعمل هذا في بناء الإصدار وداخل CI.\n\nالخيارات:\n --format png|jpg صيغة الإخراج (الافتراضي: png)\n --jpg اختصار لـ --format jpg\n --scale <0.1-1> تحجيم الإخراج (الافتراضي: 1)\n --max-width <64-8192> تحديد أقصى عرض للإخراج مع الحفاظ على نسبة الأبعاد\n --quality <0.1-1> جودة JPEG (الافتراضي: 0.8، وتُهمل مع png)\n --region تصوير جزء من النافذة، بوحدات نقاط النافذة\n --out الكتابة هنا بدلاً من دليل مؤقت\n --label تسمية الملف\n --caption رسم تعليق داخل الصورة\n --window النافذة المراد تصويرها (الافتراضي: النافذة الأمامية)\n\nالإخراج: `x `، أو الاستجابة الكاملة مع --json.\n\nالمنطقة هي نفس المستطيل الذي يصوّره `cmux record --region`، لذا يمكن تصوير\nتفصيل لوحظ في مقطع بالأرقام الأربعة نفسها.\n\nمثال:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" + "value": "الاستخدام: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nيرسل رسالة إلى الوكيل الذي يعمل في مساحة عمل أو سطح آخر. يوصلها cmux\nعبر خطافات ذلك الوكيل، ولا يكتبها أبدًا في طرفيته،\nلذا لا يمكن أن تقع داخل موجّه مكتوب نصفه. تستيقظ جلسة Claude Code الخاملة\nلقراءتها، وتقرأها الجلسة المشغولة في خطوتها التالية.\n\n هو معرّف أو مرجع مساحة عمل أو سطح (workspace:2، surface:5)، أو\nعنوان مساحة عمل (مطابقة تامة أولًا، ثم بادئة فريدة). استخدم - لقراءة النص من\nstdin. القيمة الافتراضية لـ --from هي عنوان مساحة العمل المرسِلة.\n\nأمثلة:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux shot [Flags]\n\nEin cmux-Fenster oder einen Teil davon als png oder jpeg aufnehmen. Nur\ncmux-eigene Fenster sind aufnehmbar, und es ist keine Berechtigung für\nBildschirmaufnahmen nötig, das geht also im Release-Build und in der CI.\n\nFlags:\n --format png|jpg Ausgabeformat (Standard: png)\n --jpg Kurzform für --format jpg\n --scale <0.1-1> Ausgabe skalieren (Standard: 1)\n --max-width <64-8192> Ausgabebreite begrenzen, Seitenverhältnis bleibt\n --quality <0.1-1> JPEG-Qualität (Standard: 0.8, für png ignoriert)\n --region Teil des Fensters aufnehmen, in Fensterpunkten\n --out Hierhin schreiben statt in ein temporäres Verzeichnis\n --label Datei benennen\n --caption Eine Bildunterschrift ins Bild zeichnen\n --window Fenster zum Aufnehmen (Standard: das vorderste)\n\nAusgabe: `x `, oder mit --json die vollständige Antwort.\n\nEine Region ist dasselbe Rechteck, das `cmux record --region` filmt: ein im\nClip entdecktes Detail lässt sich mit denselben vier Zahlen aufnehmen.\n\nBeispiel:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Uso: cmux shot [flags]\n\nCaptura una ventana de cmux, o una región de ella, en png o jpeg. Solo se\npueden capturar las ventanas propias de cmux, y no interviene ningún permiso\nde grabación de pantalla, así que funciona en una release y dentro de CI.\n\nFlags:\n --format png|jpg Formato de salida (predeterminado: png)\n --jpg Atajo para --format jpg\n --scale <0.1-1> Escala la salida (predeterminado: 1)\n --max-width <64-8192> Limita el ancho de salida manteniendo la proporción\n --quality <0.1-1> Calidad JPEG (predeterminado: 0.8, se ignora en png)\n --region Captura parte de la ventana, en puntos de ventana\n --out Escribe aquí en lugar de en un directorio temporal\n --label Da nombre al archivo\n --caption Dibuja un rótulo dentro de la imagen\n --window Ventana que se captura (predeterminado: la del frente)\n\nSalida: `x `, o la respuesta completa con --json.\n\nUna región es el mismo rectángulo que filma `cmux record --region`, así que un\ndetalle visto en un clip se captura con los mismos cuatro números.\n\nEjemplo:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Utilisation : cmux shot [flags]\n\nCapture une fenêtre cmux, ou une partie de celle-ci, en png ou en jpeg.\nSeules les fenêtres de cmux peuvent être capturées, et aucune autorisation\nd'enregistrement d'écran n'intervient : cela marche en release et dans la CI.\n\nFlags :\n --format png|jpg Format de sortie (par défaut : png)\n --jpg Raccourci pour --format jpg\n --scale <0.1-1> Met la sortie à l'échelle (par défaut : 1)\n --max-width <64-8192> Limite la largeur de sortie en gardant les proportions\n --quality <0.1-1> Qualité JPEG (par défaut : 0.8, ignorée pour png)\n --region Capture une partie de la fenêtre, en points de fenêtre\n --out Écrit ici au lieu d'un répertoire temporaire\n --label Nomme le fichier\n --caption Dessine une légende dans l'image\n --window Fenêtre à capturer (par défaut : celle au premier plan)\n\nSortie : `x `, ou la réponse complète avec --json.\n\nUne région est le même rectangle que filme `cmux record --region` : un détail\nrepéré dans un clip se capture avec les mêmes quatre nombres.\n\nExemple :\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "使用方法: cmux shot [flags]\n\ncmux のウィンドウ、またはその一部の領域を png または jpeg で撮ります。\n撮れるのは cmux 自身のウィンドウだけで、画面収録の権限は必要ないので、\nリリースビルドでも CI の中でも動きます。\n\nフラグ:\n --format png|jpg 出力形式(既定: png)\n --jpg --format jpg の短縮形\n --scale <0.1-1> 出力を拡大縮小(既定: 1)\n --max-width <64-8192> 縦横比を保ったまま出力の幅を制限\n --quality <0.1-1> JPEG の品質(既定: 0.8、png では無視)\n --region ウィンドウの一部だけを撮る(ウィンドウポイント単位)\n --out 一時ディレクトリではなくこのパスに書き出す\n --label ファイルに名前を付ける\n --caption 画像にキャプションを描画する\n --window 撮るウィンドウ(既定: 最前面のウィンドウ)\n\n出力: `x `、または --json で完全なレスポンス。\n\n領域は `cmux record --region` が撮るのと同じ矩形なので、クリップで見つけた\n細部を同じ 4 つの数字で撮れます。\n\n例:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사용법: cmux shot [flags]\n\ncmux 창이나 창의 일부 영역을 png 또는 jpeg로 캡처합니다. cmux 자신의\n창만 캡처할 수 있고, 화면 기록 권한이 필요하지 않으므로 릴리스 빌드와\nCI 안에서도 동작합니다.\n\n플래그:\n --format png|jpg 출력 형식 (기본값: png)\n --jpg --format jpg의 단축형\n --scale <0.1-1> 출력 크기 조정 (기본값: 1)\n --max-width <64-8192> 가로 세로 비율을 유지하며 출력 너비 제한\n --quality <0.1-1> JPEG 품질 (기본값: 0.8, png에서는 무시)\n --region 창의 일부만 캡처, 창 포인트 단위\n --out 임시 디렉터리 대신 이 경로에 기록\n --label 파일 이름 지정\n --caption 이미지에 캡션 그리기\n --window 캡처할 창 (기본값: 맨 앞 창)\n\n출력: `x `, 또는 --json으로 전체 응답.\n\n영역은 `cmux record --region`이 촬영하는 사각형과 같으므로, 클립에서 본\n세부를 같은 네 숫자로 캡처할 수 있습니다.\n\n예:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "使用方法: cmux shot [flags]\n\n将 cmux 窗口或其中一块区域截取为 png 或 jpeg。\n只能截取 cmux 自己的窗口,而且不需要任何屏幕录制权限,\n因此在 release 构建和 CI 中都能用。\n\n标志:\n --format png|jpg 输出格式(默认: png)\n --jpg --format jpg 的简写\n --scale <0.1-1> 缩放输出(默认: 1)\n --max-width <64-8192> 限制输出宽度并保持宽高比\n --quality <0.1-1> JPEG 质量(默认: 0.8,png 会忽略)\n --region 只截取窗口的一部分,以窗口点为单位\n --out 写入此处而非临时目录\n --label 为文件命名\n --caption 在图片上绘制字幕\n --window 要截取的窗口(默认: 最前面的窗口)\n\n输出: `x `,或用 --json 获取完整响应。\n\n区域和 `cmux record --region` 拍摄的矩形相同,因此在视频里看到的细节\n可以用同样的四个数字截取。\n\n示例:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" + "value": "Verwendung: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nSendet eine Nachricht an den Agenten in einem anderen Workspace oder einer anderen Oberfläche. cmux\nliefert sie über die Hooks dieses Agenten aus, nie durch Tippen in sein Terminal,\ndaher kann sie nicht in einem halb getippten Prompt landen. Eine untätige Claude-Code-Sitzung wacht\nauf, um sie zu lesen; eine beschäftigte liest sie bei ihrem nächsten Schritt.\n\n ist eine Workspace- oder Oberflächen-ID bzw. -Referenz (workspace:2, surface:5) oder ein\nWorkspace-Titel (zuerst exakt, dann ein eindeutiges Präfix). Mit - wird der Text von\nstdin gelesen. --from ist standardmäßig der Titel des sendenden Workspace.\n\nBeispiele:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "使用方法: cmux shot [flags]\n\n將 cmux 視窗或其中一塊區域擷取成 png 或 jpeg。\n只能擷取 cmux 自己的視窗,而且不需要任何螢幕錄製權限,\n因此在 release 版本與 CI 內都能用。\n\n旗標:\n --format png|jpg 輸出格式(預設: png)\n --jpg --format jpg 的簡寫\n --scale <0.1-1> 縮放輸出(預設: 1)\n --max-width <64-8192> 限制輸出寬度並保持長寬比\n --quality <0.1-1> JPEG 品質(預設: 0.8,png 會忽略)\n --region 只擷取視窗的一部分,以視窗點為單位\n --out 寫入此處而非暫存目錄\n --label 為檔案命名\n --caption 在圖片上畫出字幕\n --window 要擷取的視窗(預設: 最前面的視窗)\n\n輸出: `x `,或用 --json 取得完整回應。\n\n區域和 `cmux record --region` 拍攝的矩形相同,所以在影片裡看到的細節\n可以用同樣的四個數字擷取。\n\n範例:\n cmux shot --label settings-sheet\n cmux shot --region 0,0,420,900 --out ~/sidebar.png" - } - } - } - }, - "cli.shot.error.formatConflict": { - "extractionState": "manual", - "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "shot: --jpg conflicts with --format" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "shot: لا يمكن دمج --jpg مع --format" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "shot: --jpg kann nicht mit --format kombiniert werden" + "value": "Usage: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nSend a message to the agent running in another workspace or surface. cmux\ndelivers it through that agent's hooks, never by typing into its terminal,\nso it cannot land in a half-typed prompt. An idle Claude Code session wakes\nup to read it; a busy one reads it at its next step.\n\n is a workspace or surface id or ref (workspace:2, surface:5), or a\nworkspace title (exact, then a unique prefix). Use - to read the text from\nstdin. --from defaults to the sending workspace's title.\n\nExamples:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "es": { "stringUnit": { "state": "translated", - "value": "shot: --jpg no se puede combinar con --format" + "value": "Uso: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nEnvía un mensaje al agente que se ejecuta en otro espacio de trabajo o superficie. cmux\nlo entrega a través de los hooks de ese agente, nunca escribiendo en su terminal,\npor lo que no puede acabar en un prompt a medio escribir. Una sesión inactiva de Claude Code se despierta\npara leerlo; una ocupada lo lee en su siguiente paso.\n\n es un ID o referencia de espacio de trabajo o superficie (workspace:2, surface:5), o un\ntítulo de espacio de trabajo (exacto, luego un prefijo único). Usa - para leer el texto desde\nstdin. --from usa por defecto el título del espacio de trabajo que envía.\n\nEjemplos:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "fr": { "stringUnit": { "state": "translated", - "value": "shot : --jpg ne peut pas être combiné avec --format" + "value": "Utilisation : cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nEnvoie un message à l'agent d'un autre espace de travail ou d'une autre surface. cmux\nle remet via les hooks de cet agent, jamais en tapant dans son terminal,\nil ne peut donc pas atterrir dans un prompt à moitié tapé. Une session Claude Code inactive se réveille\npour le lire ; une session occupée le lit à sa prochaine étape.\n\n est un identifiant ou une référence d'espace de travail ou de surface (workspace:2, surface:5), ou un\ntitre d'espace de travail (exact, puis un préfixe unique). Utilisez - pour lire le texte depuis\nstdin. Par défaut, --from vaut le titre de l'espace de travail expéditeur.\n\nExemples :\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "ja": { "stringUnit": { "state": "translated", - "value": "shot: --jpg は --format と併用できません" + "value": "使い方: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n別のワークスペースまたはサーフェスで動いているエージェントにメッセージを送ります。cmux は\nそのエージェントのフック経由で届け、ターミナルに打ち込むことはないので、\n入力途中のプロンプトに紛れ込みません。アイドル中の Claude Code セッションは起きて\n読み、作業中のセッションは次のステップで読みます。\n\n はワークスペースかサーフェスの ID または参照(workspace:2、surface:5)、または\nワークスペースのタイトル(完全一致、次に一意な前方一致)です。- を指定すると\nstdin からテキストを読みます。--from の既定値は送信元ワークスペースのタイトルです。\n\n例:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "ko": { "stringUnit": { "state": "translated", - "value": "shot: --jpg은 --format과 함께 사용할 수 없습니다" + "value": "사용법: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n다른 워크스페이스나 표면에서 실행 중인 에이전트에게 메시지를 보냅니다. cmux는\n해당 에이전트의 훅을 통해 전달하며, 절대 터미널에 입력하지 않으므로\n반쯤 입력된 프롬프트에 섞일 수 없습니다. 유휴 상태의 Claude Code 세션은 깨어나서\n읽고, 작업 중인 세션은 다음 단계에서 읽습니다.\n\n은 워크스페이스나 표면의 ID 또는 참조(workspace:2, surface:5), 또는\n워크스페이스 제목(먼저 정확히 일치, 그다음 고유한 접두사)입니다. -를 쓰면\nstdin에서 텍스트를 읽습니다. --from의 기본값은 보내는 워크스페이스의 제목입니다.\n\n예시:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "shot: --jpg 不能与 --format 同用" + "value": "用法: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n向在另一个工作区或表面中运行的代理发送消息。cmux\n通过该代理的 hook 投递消息,绝不会向它的终端输入按键,\n因此消息不会落入输入到一半的提示中。空闲的 Claude Code 会话会醒来\n阅读它;忙碌的会话会在下一步阅读。\n\n 是工作区或表面的 ID 或引用(workspace:2、surface:5),或\n工作区标题(先完全匹配,再找唯一前缀)。使用 - 从\nstdin 读取文本。--from 默认为发送端工作区的标题。\n\n示例:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "shot: --jpg 無法與 --format 同時使用" + "value": "用法: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n傳送訊息給在另一個工作區或表面中執行的代理。cmux\n透過該代理的 hook 遞送訊息,絕不會打字到它的終端機,\n因此訊息不會落入打到一半的提示中。閒置的 Claude Code 工作階段會醒來\n閱讀它;忙碌中的工作階段會在下一步閱讀。\n\n 是工作區或表面的 ID 或參照(workspace:2、surface:5),或\n工作區標題(先完全符合,再找唯一前綴)。使用 - 從\nstdin 讀取文字。--from 預設為傳送端工作區的標題。\n\n範例:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" } } } }, - "cli.shot.error.unexpectedArguments": { + "socket.agentMessage.error.bodyTooLarge": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "shot: unexpected arguments: %@" + "value": "The message is too long. The limit is 32 KiB." } }, "ar": { "stringUnit": { "state": "translated", - "value": "shot: وسائط غير متوقعة: %@" + "value": "الرسالة طويلة جدًا. الحد الأقصى 32 كيلوبايت." } }, "de": { "stringUnit": { "state": "translated", - "value": "shot: unerwartete Argumente: %@" + "value": "Die Nachricht ist zu lang. Die Grenze liegt bei 32 KiB." } }, "es": { "stringUnit": { "state": "translated", - "value": "shot: argumentos inesperados: %@" + "value": "El mensaje es demasiado largo. El límite es de 32 KiB." } }, "fr": { "stringUnit": { "state": "translated", - "value": "shot : arguments inattendus : %@" + "value": "Le message est trop long. La limite est de 32 Kio." } }, "ja": { "stringUnit": { "state": "translated", - "value": "shot: 予期しない引数: %@" + "value": "メッセージが長すぎます。上限は 32 KiB です。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "shot: 예기치 않은 인수: %@" + "value": "메시지가 너무 깁니다. 한도는 32KiB입니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "shot: 多余的参数: %@" + "value": "消息太长。上限为 32 KiB。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "shot: 未預期的引數: %@" - } - } - } - }, - "cli.usage.shot": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "shot [--format png|jpg] [--jpg] [--scale ] [--max-width ] [--quality ] [--region ] [--out ] [--label ] [--caption ] [--window ]" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "shot [--format png|jpg] [--jpg] [--scale ] [--max-width ] [--quality ] [--region ] [--out ] [--label ] [--caption ] [--window ]" + "value": "訊息太長。上限為 32 KiB。" } } } }, - "actions.discovery.typeDetail": { + "socket.agentMessage.error.controlCharacter": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Action type: %@" + "value": "The message contains control characters. Only text, newlines and tabs are allowed." } }, "ar": { "stringUnit": { "state": "translated", - "value": "نوع الإجراء: %@" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Vrsta radnje: %@" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Handlingstype: %@" + "value": "تحتوي الرسالة على أحرف تحكم. لا يُسمح إلا بالنص وفواصل الأسطر وعلامات الجدولة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Aktionstyp: %@" + "value": "Die Nachricht enthält Steuerzeichen. Erlaubt sind nur Text, Zeilenumbrüche und Tabulatoren." } }, "es": { "stringUnit": { "state": "translated", - "value": "Tipo de acción: %@" + "value": "El mensaje contiene caracteres de control. Solo se permiten texto, saltos de línea y tabulaciones." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Type d’action : %@" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Tipo di azione: %@" + "value": "Le message contient des caractères de contrôle. Seuls le texte, les retours à la ligne et les tabulations sont autorisés." } }, "ja": { "stringUnit": { "state": "translated", - "value": "アクションの種類:%@" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ប្រភេទសកម្មភាព៖ %@" + "value": "メッセージに制御文字が含まれています。使えるのはテキスト、改行、タブだけです。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "작업 유형: %@" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Handlingstype: %@" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Typ działania: %@" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Tipo de ação: %@" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Тип действия: %@" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ประเภทการดำเนินการ: %@" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Eylem türü: %@" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Тип дії: %@" + "value": "메시지에 제어 문자가 있습니다. 텍스트, 줄바꿈, 탭만 허용됩니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "操作类型:%@" + "value": "消息包含控制字符。只允许文本、换行和制表符。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "動作類型:%@" + "value": "訊息包含控制字元。只允許文字、換行和定位字元。" } } } }, - "actions.discovery.shortcutDetail": { + "socket.agentMessage.error.emptyBody": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Shortcut: %@" + "value": "The message is empty." } }, "ar": { "stringUnit": { "state": "translated", - "value": "اختصار لوحة المفاتيح: %@" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Prečica: %@" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Genvej: %@" + "value": "الرسالة فارغة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Tastenkürzel: %@" + "value": "Die Nachricht ist leer." } }, "es": { "stringUnit": { "state": "translated", - "value": "Atajo: %@" + "value": "El mensaje está vacío." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Raccourci : %@" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Scorciatoia: %@" + "value": "Le message est vide." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ショートカット:%@" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ផ្លូវកាត់៖ %@" + "value": "メッセージが空です。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "단축키: %@" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Snarvei: %@" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Skrót: %@" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Atalho: %@" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Сочетание клавиш: %@" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "แป้นพิมพ์ลัด: %@" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Klavye kısayolu: %@" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Сполучення клавіш: %@" + "value": "메시지가 비어 있습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "快捷键:%@" + "value": "消息为空。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "快速鍵:%@" + "value": "訊息是空的。" } } } }, - "actions.discovery.menuTitle": { + "socket.agentMessage.error.invalidSender": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Actions · cmux.json…" + "value": "The sender name must be one line of at most 64 characters." } }, "ar": { "stringUnit": { "state": "translated", - "value": "الإجراءات · cmux.json…" + "value": "يجب أن يكون اسم المرسل سطرًا واحدًا لا يتجاوز 64 حرفًا." } }, "de": { "stringUnit": { "state": "translated", - "value": "Aktionen · cmux.json…" + "value": "Der Absendername muss eine einzelne Zeile mit höchstens 64 Zeichen sein." } }, "es": { "stringUnit": { "state": "translated", - "value": "Acciones · cmux.json…" + "value": "El nombre del remitente debe ser una sola línea de 64 caracteres como máximo." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Actions · cmux.json…" + "value": "Le nom de l'expéditeur doit tenir sur une ligne de 64 caractères au maximum." } }, "ja": { "stringUnit": { "state": "translated", - "value": "アクション · cmux.json…" + "value": "送信者名は 64 文字以内の 1 行にしてください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "작업 · cmux.json…" + "value": "보낸 사람 이름은 64자 이하의 한 줄이어야 합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "操作 · cmux.json…" + "value": "发送者名称必须是一行,最多 64 个字符。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "動作 · cmux.json…" + "value": "寄件者名稱必須是一行,最多 64 個字元。" } } } }, - "actions.discovery.dialogTitle": { + "socket.agentMessage.error.invalidState": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Actions · cmux.json" + "value": "State must be queued, delivered or read." } }, "ar": { "stringUnit": { "state": "translated", - "value": "الإجراءات · cmux.json" + "value": "يجب أن تكون الحالة queued أو delivered أو read." } }, "de": { "stringUnit": { "state": "translated", - "value": "Aktionen · cmux.json" + "value": "Der Status muss queued, delivered oder read sein." } }, "es": { "stringUnit": { "state": "translated", - "value": "Acciones · cmux.json" + "value": "El estado debe ser queued, delivered o read." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Actions · cmux.json" + "value": "L'état doit être queued, delivered ou read." } }, "ja": { "stringUnit": { "state": "translated", - "value": "アクション · cmux.json" + "value": "状態は queued、delivered、read のいずれかにしてください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "작업 · cmux.json" + "value": "상태는 queued, delivered, read 중 하나여야 합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "操作 · cmux.json" + "value": "状态必须是 queued、delivered 或 read。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "動作 · cmux.json" + "value": "狀態必須是 queued、delivered 或 read。" } } } }, - "actions.discovery.empty": { + "socket.agentMessage.error.missingSurface": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No configured actions." + "value": "A surface_id UUID is required." } }, "ar": { "stringUnit": { "state": "translated", - "value": "لا توجد إجراءات مُعدّة." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Nema konfigurisanih radnji." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Ingen konfigurerede handlinger." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Keine konfigurierten Aktionen." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No hay acciones configuradas." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Aucune action configurée." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Nessuna azione configurata." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "設定済みのアクションはありません。" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "មិនមានសកម្មភាពដែលបានកំណត់រចនាសម្ព័ន្ធទេ។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "구성된 작업이 없습니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Ingen konfigurerte handlinger." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Brak skonfigurowanych działań." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Nenhuma ação configurada." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Нет настроенных действий." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ไม่มีการดำเนินการที่กำหนดค่าไว้" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Yapılandırılmış eylem yok." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Немає налаштованих дій." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有已配置的操作。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "沒有已設定的動作。" - } - } - } - }, - "agentWake.banner.close": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Close" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "閉じる" + "value": "يلزم تحديد surface_id بصيغة UUID." } }, "de": { "stringUnit": { "state": "translated", - "value": "Schließen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Fermer" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إغلاق" + "value": "Eine surface_id-UUID ist erforderlich." } }, "es": { "stringUnit": { "state": "translated", - "value": "Cerrar" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "關閉" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "닫기" - } - } - } - }, - "agentWake.banner.copy": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Copy" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピー" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopieren" + "value": "Se requiere un UUID en surface_id." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Copier" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "نسخ" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Copiar" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "複製" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사" - } - } - } - }, - "agentWake.banner.hideCommand": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Hide command" + "value": "Un UUID surface_id est requis." } }, "ja": { "stringUnit": { "state": "translated", - "value": "コマンドを隠す" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Befehl ausblenden" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Masquer la commande" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إخفاء الأمر" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Ocultar comando" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "隱藏指令" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "隐藏命令" + "value": "surface_id の UUID が必要です。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "명령 숨기기" - } - } - } - }, - "agentWake.banner.message": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "%@ didn't resume after waking." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%@ はスリープ解除後に再開しませんでした。" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "%@ wurde nach dem Aufwecken nicht fortgesetzt." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "%@ n’a pas repris après le réveil." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف %@ بعد الإيقاظ." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "%@ no se reanudó después de despertar." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "%@ 在喚醒後未能恢復。" + "value": "surface_id UUID가 필요합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%@ 在唤醒后未能恢复。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "%@이(가) 깨운 후 다시 시작되지 않았습니다." - } - } - } - }, - "agentWake.banner.retry": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Retry" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "再試行" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Erneut versuchen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Réessayer" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعادة المحاولة" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reintentar" + "value": "需要 surface_id UUID。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "重試" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重试" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "재시도" + "value": "需要 surface_id UUID。" } } } }, - "agentWake.banner.showCommand": { + "socket.agentMessage.error.missingTarget": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Show command" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コマンドを表示" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Befehl anzeigen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Afficher la commande" + "value": "Give a target workspace or surface, or a message id to reply to." } }, "ar": { "stringUnit": { "state": "translated", - "value": "إظهار الأمر" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Mostrar comando" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "顯示指令" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示命令" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "명령 보기" - } - } - } - }, - "agentWake.notification.title": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "%@ didn't resume" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%@ が再開しませんでした" + "value": "حدّد مساحة عمل أو سطحًا هدفًا، أو معرّف رسالة للرد عليها." } }, "de": { "stringUnit": { "state": "translated", - "value": "%@ wurde nicht fortgesetzt" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "%@ n’a pas repris" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف %@" + "value": "Gib einen Ziel-Workspace oder eine Zieloberfläche an, oder eine Nachrichten-ID, auf die geantwortet werden soll." } }, "es": { "stringUnit": { "state": "translated", - "value": "%@ no se reanudó" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "%@ 未能恢復" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 未能恢复" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "%@이(가) 다시 시작되지 않았습니다" - } - } - } - }, - "agentWake.reason.didNotStart": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "It did not start within 90 seconds." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "90 秒以内に起動しませんでした。" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Es wurde nicht innerhalb von 90 Sekunden gestartet." + "value": "Indica un espacio de trabajo o una superficie de destino, o el ID de un mensaje al que responder." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Il n’a pas démarré dans les 90 secondes." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لم يبدأ خلال 90 ثانية." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No se inició en 90 segundos." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "未在 90 秒內啟動。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未在 90 秒内启动。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "90초 안에 시작되지 않았습니다." - } - } - } - }, - "agentWake.reason.exitedBeforeStart": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Its resume command exited." + "value": "Indiquez un espace de travail ou une surface cible, ou l'identifiant du message auquel répondre." } }, "ja": { "stringUnit": { "state": "translated", - "value": "再開コマンドが終了しました。" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Sein Fortsetzungsbefehl wurde beendet." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Sa commande de reprise s’est terminée." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "انتهى أمر الاستئناف الخاص به." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Su comando de reanudación terminó." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "其恢復指令已結束。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "其恢复命令已退出。" + "value": "宛先のワークスペースかサーフェス、または返信するメッセージの ID を指定してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "재개 명령이 종료되었습니다." - } - } - } - }, - "agentWake.status.multiple": { - "extractionState": "manual", - "localizations": { - "en": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%ld agent didn't resume" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld agents didn't resume" - } - } - } - } - }, - "ja": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld 個のエージェントが再開しませんでした" - } - } - } - } - }, - "de": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%ld Agent wurde nicht fortgesetzt" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld Agenten wurden nicht fortgesetzt" - } - } - } - } - }, - "fr": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%ld agent n’a pas repris" - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "%ld agents n’ont pas repris" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld agents n’ont pas repris" - } - } - } - } - }, - "ar": { - "variations": { - "plural": { - "zero": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف أي وكيل (%ld)" - } - }, - "one": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف وكيل واحد (%ld)" - } - }, - "two": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف وكيلان (%ld)" - } - }, - "few": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف %ld وكلاء" - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف %ld وكيلًا" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "لم يُستأنف %ld وكيل" - } - } - } - } - }, - "es": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%ld agente no se reanudó" - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "%ld agentes no se reanudaron" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld agentes no se reanudaron" - } - } - } - } - }, - "zh-Hant": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld 個代理程式未能恢復" - } - } - } + "value": "대상 워크스페이스나 표면, 또는 답장할 메시지 ID를 지정하세요." } }, "zh-Hans": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%ld 个代理未能恢复" - } - } - } - } - }, - "ko": { - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "에이전트 %ld개가 다시 시작되지 않았습니다" - } - } - } - } - } - } - }, - "agentWake.status.single": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Agent didn't resume" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントが再開しませんでした" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Agent wurde nicht fortgesetzt" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "L’agent n’a pas repris" - } - }, - "ar": { "stringUnit": { "state": "translated", - "value": "لم يُستأنف الوكيل" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "El agente no se reanudó" + "value": "请指定目标工作区或表面,或要回复的消息 ID。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "代理程式未能恢復" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "代理未能恢复" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "에이전트가 다시 시작되지 않았습니다" + "value": "請指定目標工作區或表面,或要回覆的訊息 ID。" } } } }, - "cloud.operation.failure.machineAsleep": { + "socket.agentMessage.error.noReplyAddress": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This machine is asleep. Wake it to connect." + "value": "That message has no sender surface to reply to." } }, "ar": { "stringUnit": { "state": "translated", - "value": "هذا الجهاز في وضع السكون. أوقظه للاتصال." + "value": "لا يوجد لهذه الرسالة سطح مرسل يمكن الرد عليه." } }, "de": { "stringUnit": { "state": "translated", - "value": "Diese Maschine ist im Ruhezustand. Wecken Sie sie auf, um eine Verbindung herzustellen." + "value": "Diese Nachricht hat keine Absenderoberfläche, an die geantwortet werden kann." } }, "es": { "stringUnit": { "state": "translated", - "value": "Esta máquina está en reposo. Actívala para conectarte." + "value": "Ese mensaje no tiene una superficie de remitente a la que responder." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Cette machine est en veille. Réveillez-la pour vous connecter." + "value": "Ce message n'a pas de surface d'expéditeur à laquelle répondre." } }, "ja": { "stringUnit": { "state": "translated", - "value": "このマシンはスリープ中です。接続するには起動してください。" + "value": "このメッセージには返信先となる送信元サーフェスがありません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 머신은 잠자기 상태입니다. 연결하려면 깨우세요." + "value": "이 메시지에는 답장할 보낸 사람 표면이 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此机器处于睡眠状态。唤醒它以连接。" + "value": "该消息没有可回复的发送者表面。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "這部機器目前處於睡眠狀態。喚醒它以連線。" + "value": "該訊息沒有可回覆的寄件者表面。" } } } }, - "cloud.operation.failure.machineUnavailable": { + "socket.agentMessage.error.notSaved": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux cannot reach the Cloud service for this machine right now." + "value": "The message was not sent: cmux could not save it to the message journal." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يتعذر على cmux الوصول إلى خدمة Cloud لهذه الآلة في الوقت الحالي." + "value": "لم يتم إرسال الرسالة: تعذّر على cmux حفظها في سجل الرسائل." } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux kann den Cloud-Dienst für diese Maschine derzeit nicht erreichen." + "value": "Die Nachricht wurde nicht gesendet: cmux konnte sie nicht im Nachrichtenjournal speichern." } }, "es": { "stringUnit": { "state": "translated", - "value": "cmux no puede acceder al servicio Cloud para esta máquina en este momento." + "value": "El mensaje no se envió: cmux no pudo guardarlo en el registro de mensajes." } }, "fr": { "stringUnit": { "state": "translated", - "value": "cmux ne peut pas joindre le service Cloud pour cette machine pour le moment." + "value": "Le message n’a pas été envoyé : cmux n’a pas pu l’enregistrer dans le journal des messages." } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux は現在、このマシンの Cloud サービスに接続できません。" + "value": "メッセージは送信されませんでした: cmux がメッセージジャーナルに保存できませんでした。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux가 현재 이 머신의 Cloud 서비스에 연결할 수 없습니다." + "value": "메시지를 보내지 못했습니다: cmux가 메시지 저널에 저장하지 못했습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 目前无法连接到此机器的 Cloud 服务。" + "value": "消息未发送:cmux 无法将其保存到消息日志。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux 目前無法連線到這部機器的 Cloud 服務。" + "value": "訊息未傳送:cmux 無法將其儲存到訊息日誌。" } } } }, - "cli.send.error.dialogOpen": { + "socket.agentMessage.error.targetNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@ is waiting on a question or dialog, so nothing was sent. Retry once it is answered, or pass --force to send anyway." + "value": "No workspace or surface matches that target." } }, "ar": { "stringUnit": { "state": "translated", - "value": "%1$@: ينتظر %2$@ إجابة على سؤال أو مربع حوار، لذا لم يُرسل شيء. أعد المحاولة بعد الإجابة، أو مرّر --force للإرسال على أي حال." + "value": "لا توجد مساحة عمل أو سطح يطابق هذا الهدف." } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@ wartet auf eine Frage oder einen Dialog, daher wurde nichts gesendet. Versuche es erneut, sobald sie beantwortet ist, oder übergib --force, um trotzdem zu senden." + "value": "Kein Workspace und keine Oberfläche passt zu diesem Ziel." } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@ está esperando una pregunta o un diálogo, así que no se envió nada. Vuelve a intentarlo cuando se responda, o pasa --force para enviarlo de todos modos." + "value": "Ningún espacio de trabajo ni superficie coincide con ese destino." } }, "fr": { "stringUnit": { "state": "translated", - "value": "%1$@ : %2$@ attend la réponse à une question ou à une boîte de dialogue, rien n'a donc été envoyé. Réessayez une fois la réponse donnée, ou passez --force pour envoyer quand même." + "value": "Aucun espace de travail ni aucune surface ne correspond à cette cible." } }, "ja": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@ は質問またはダイアログへの応答を待っているため、何も送信しませんでした。応答後に再試行するか、--force を指定して送信してください。" + "value": "その宛先に一致するワークスペースもサーフェスもありません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@에서 질문 또는 대화상자가 응답을 기다리고 있어 아무것도 보내지 않았습니다. 응답한 후 다시 시도하거나 --force를 지정해 그래도 보내세요." + "value": "해당 대상과 일치하는 워크스페이스나 표면이 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@:%2$@ 正在等待问题或对话框的回应,因此未发送任何内容。请在回应后重试,或传入 --force 强制发送。" + "value": "没有与该目标匹配的工作区或表面。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@:%2$@ 正在等待問題或對話框的回應,因此未傳送任何內容。請在回應後重試,或傳入 --force 強制傳送。" + "value": "沒有符合該目標的工作區或表面。" } } } }, - "cli.send.error.draftPresent": { + "socket.agentMessage.error.unknownMessage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@: the agent prompt in %2$@ holds text someone is typing, so nothing was sent. Retry when the prompt is clear, or pass --force to send anyway." + "value": "No message with id %@." } }, "ar": { "stringUnit": { "state": "translated", - "value": "%1$@: تحتوي مطالبة الوكيل في %2$@ على نص يكتبه شخص ما، لذا لم يُرسل شيء. أعد المحاولة عندما تصبح المطالبة فارغة، أو مرّر --force للإرسال على أي حال." + "value": "لا توجد رسالة بالمعرّف %@." } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@: Die Agent-Eingabe in %2$@ enthält Text, den jemand gerade tippt, daher wurde nichts gesendet. Versuche es erneut, wenn die Eingabe leer ist, oder übergib --force, um trotzdem zu senden." + "value": "Keine Nachricht mit der ID %@." } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@: el prompt del agente en %2$@ contiene texto que alguien está escribiendo, así que no se envió nada. Vuelve a intentarlo cuando el prompt esté vacío, o pasa --force para enviarlo de todos modos." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "%1$@ : l'invite de l'agent dans %2$@ contient du texte en cours de saisie, rien n'a donc été envoyé. Réessayez quand l'invite est vide, ou passez --force pour envoyer quand même." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%1$@: %2$@ のエージェントのプロンプトに誰かが入力中のテキストがあるため、何も送信しませんでした。プロンプトが空になってから再試行するか、--force を指定して送信してください。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "%1$@: %2$@의 에이전트 프롬프트에 누군가 입력 중인 텍스트가 있어 아무것도 보내지 않았습니다. 프롬프트가 비면 다시 시도하거나 --force를 지정해 그래도 보내세요." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%1$@:%2$@ 中的代理提示里有人正在输入文本,因此未发送任何内容。请在提示清空后重试,或传入 --force 强制发送。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "%1$@:%2$@ 中的代理程式提示裡有人正在輸入文字,因此未傳送任何內容。請在提示清空後重試,或傳入 --force 強制傳送。" - } - } - } - }, - "agent.autoResume.status": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Auto-resumed ×%lld" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Automatisch fortgesetzt ×%lld" + "value": "No hay ningún mensaje con el ID %@." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Reprise automatique ×%lld" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "استئناف تلقائي ×%lld" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reanudado automáticamente ×%lld" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "已自動繼續 ×%lld" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已自动继续 ×%lld" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "자동 재개 ×%lld" + "value": "Aucun message avec l'identifiant %@." } }, "ja": { "stringUnit": { "state": "translated", - "value": "自動再開 ×%lld" - } - } - } - }, - "settings.automation.agentAutoResume": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Auto-Resume Agents After Errors" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Agenten nach Fehlern automatisch fortsetzen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Relancer automatiquement les agents après une erreur" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "استئناف الوكلاء تلقائيًا بعد الأخطاء" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reanudar agentes automáticamente tras errores" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "錯誤後自動恢復代理" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "出错后自动恢复代理" + "value": "ID が %@ のメッセージはありません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "오류 후 에이전트 자동 재개" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エラー後にエージェントを自動再開" - } - } - } - }, - "settings.automation.agentAutoResume.subtitle": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Send “continue” when an agent's turn ends on a retryable error such as model capacity or a dropped connection." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Sendet „continue“, wenn der Zug eines Agenten mit einem wiederholbaren Fehler endet, etwa ausgelasteter Modellkapazität oder einer getrennten Verbindung." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Envoie « continue » quand le tour d’un agent se termine sur une erreur réessayable, comme un modèle saturé ou une connexion interrompue." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يرسل \"continue\" عندما ينتهي دور الوكيل بخطأ يمكن إعادة المحاولة بعده، مثل امتلاء سعة النموذج أو انقطاع الاتصال." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Envía «continue» cuando el turno de un agente termina con un error reintentable, como falta de capacidad del modelo o una conexión perdida." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "當代理的回合因可重試的錯誤(例如模型容量不足或連線中斷)而結束時,傳送「continue」。" + "value": "ID가 %@인 메시지가 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "当代理的回合因可重试的错误(例如模型容量不足或连接中断)而结束时,发送“continue”。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "모델 용량 초과나 연결 끊김처럼 재시도 가능한 오류로 에이전트의 턴이 끝나면 “continue”를 보냅니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "モデルの容量不足や接続切断など、再試行可能なエラーでエージェントのターンが終了したときに「continue」を送信します。" - } - } - } - }, - "settings.search.alias.setting.automation.agent-error-auto-resume": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error automatisch fortsetzen fortfahren wiederholen kapazität überlastet agent fehler" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error reprise automatique continuer réessayer capacité surchargé agent erreur" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error استئناف تلقائي متابعة إعادة المحاولة سعة حمل زائد وكيل خطأ" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error reanudar automáticamente continuar reintentar capacidad sobrecargado agente error" + "value": "没有 ID 为 %@ 的消息。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 自動恢復 繼續 重試 容量 過載 代理 錯誤" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 自动恢复 继续 重试 容量 过载 代理 错误" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 자동 재개 계속 재시도 용량 과부하 에이전트 오류" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 自動再開 続行 再試行 容量 過負荷 エージェント エラー" + "value": "沒有 ID 為 %@ 的訊息。" } } } @@ -594125,5706 +594298,5529 @@ } } }, - "machines.meter.upgrade": { + "cloud.operation.failure.machineAsleep": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Upgrade for more" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "アップグレードで追加" + "value": "This machine is asleep. Wake it to connect." } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "升级以获得更多" + "value": "هذا الجهاز في وضع السكون. أوقظه للاتصال." } }, "de": { "stringUnit": { "state": "translated", - "value": "Upgrade für mehr" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Passez au forfait supérieur pour en obtenir plus" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "ترقية للحصول على المزيد" + "value": "Diese Maschine ist im Ruhezustand. Wecken Sie sie auf, um eine Verbindung herzustellen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Actualizar para obtener más" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "升級以取得更多" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "더 사용하려면 업그레이드" - } - } - } - }, - "settings.betaFeatures.dock.subtitle": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Adds Dock to the right sidebar for custom terminal controls." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Fügt der rechten Seitenleiste das Dock für benutzerdefinierte Terminal-Steuerelemente hinzu." + "value": "Esta máquina está en reposo. Actívala para conectarte." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ajoute le Dock à la barre latérale droite pour les commandes de terminal personnalisées." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يضيف Dock إلى الشريط الجانبي الأيمن لعناصر التحكم المخصصة في الطرفية." + "value": "Cette machine est en veille. Réveillez-la pour vous connecter." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Añade el Dock a la barra lateral derecha para controles de terminal personalizados." + "value": "このマシンはスリープ中です。接続するには起動してください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "在右側邊欄加入 Dock,用來放置自訂的終端機控制項。" + "value": "이 머신은 잠자기 상태입니다. 연결하려면 깨우세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在右侧边栏添加 Dock,用来放置自定义的终端控件。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "오른쪽 사이드바에 Dock을 추가해 터미널 컨트롤을 직접 구성할 수 있습니다." + "value": "此机器处于睡眠状态。唤醒它以连接。" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "右サイドバーに Dock を追加し、ターミナル用のカスタムコントロールを配置できるようにします。" + "value": "這部機器目前處於睡眠狀態。喚醒它以連線。" } } } }, - "cli.agentMessage.noAgent": { + "cloud.operation.failure.machineUnavailable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No agent activity seen on that surface yet. The message waits until an agent there picks it up." + "value": "cmux cannot reach the Cloud service for this machine right now." } }, "ar": { "stringUnit": { "state": "translated", - "value": "لم يُرصد أي نشاط لوكيل على هذا السطح بعد. تنتظر الرسالة حتى يستلمها وكيل هناك." + "value": "يتعذر على cmux الوصول إلى خدمة Cloud لهذه الآلة في الوقت الحالي." } }, "de": { "stringUnit": { "state": "translated", - "value": "Auf dieser Oberfläche wurde noch keine Agent-Aktivität erkannt. Die Nachricht wartet, bis ein Agent dort sie abholt." + "value": "cmux kann den Cloud-Dienst für diese Maschine derzeit nicht erreichen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Todavía no se ha visto actividad de ningún agente en esa superficie. El mensaje espera hasta que un agente allí lo recoja." + "value": "cmux no puede acceder al servicio Cloud para esta máquina en este momento." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Aucune activité d'agent détectée sur cette surface pour l'instant. Le message attend qu'un agent l'y récupère." + "value": "cmux ne peut pas joindre le service Cloud pour cette machine pour le moment." } }, "ja": { "stringUnit": { "state": "translated", - "value": "そのサーフェスではまだエージェントの動きが確認されていません。そこのエージェントが受け取るまでメッセージは待機します。" + "value": "cmux は現在、このマシンの Cloud サービスに接続できません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "해당 표면에서 아직 에이전트 활동이 없습니다. 그곳의 에이전트가 가져갈 때까지 메시지가 대기합니다." + "value": "cmux가 현재 이 머신의 Cloud 서비스에 연결할 수 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "尚未在该表面看到代理活动。消息会等待,直到那里的代理取走它。" + "value": "cmux 目前无法连接到此机器的 Cloud 服务。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尚未在該表面看到代理活動。訊息會等待,直到那裡的代理取走它。" + "value": "cmux 目前無法連線到這部機器的 Cloud 服務。" } } } }, - "cli.agentMessage.queued": { + "cloudTree.menu.rename": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Queued message %@ for %@ (%@)." + "value": "Rename…" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "وُضعت الرسالة %1$@ في قائمة الانتظار لـ %2$@ (%3$@)." + "value": "名称変更…" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Nachricht %1$@ für %2$@ (%3$@) in die Warteschlange gestellt." + "value": "重命名…" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Mensaje %1$@ en cola para %2$@ (%3$@)." + "value": "Umbenennen…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Message %1$@ mis en file pour %2$@ (%3$@)." + "value": "Renommer…" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "メッセージ %1$@ を %2$@(%3$@)宛てにキューに入れました。" + "value": "إعادة التسمية…" } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "메시지 %1$@을(를) %2$@(%3$@) 앞으로 대기열에 넣었습니다." + "value": "Renombrar…" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已将消息 %1$@ 排入 %2$@(%3$@)的队列。" + "value": "重新命名…" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "已將訊息 %1$@ 排入 %2$@(%3$@)的佇列。" + "value": "이름 바꾸기…" } } } }, - "cli.help.agentInbox": { + "cloudTree.operation.rename": { "extractionState": "manual", "localizations": { - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nيعرض رسائل الوكلاء، الأحدث أولًا. بدون --surface، يعرض الرسائل لكل\nالأسطح. يضع --mark-read علامة مقروءة على الرسائل المعروضة." + "value": "Renaming %@…" } }, - "de": { + "ja": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nListet Agent-Nachrichten auf, neueste zuerst. Ohne --surface werden Nachrichten für\nalle Oberflächen aufgelistet. --mark-read markiert die aufgelisteten Nachrichten als gelesen." + "value": "%@ の名称を変更しています…" } }, - "en": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Usage: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nList agent messages, newest first. Without --surface, lists messages for\nevery surface. --mark-read marks the listed messages read." + "value": "正在重命名 %@…" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Uso: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nLista los mensajes de agentes, del más reciente al más antiguo. Sin --surface, lista los mensajes de\ntodas las superficies. --mark-read marca como leídos los mensajes listados." + "value": "%@ wird umbenannt…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nListe les messages d'agent, du plus récent au plus ancien. Sans --surface, liste les messages de\ntoutes les surfaces. --mark-read marque les messages listés comme lus." + "value": "Renommage de %@…" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "使い方: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\nエージェントのメッセージを新しい順に一覧表示します。--surface を指定しないと\nすべてのサーフェスのメッセージを表示します。--mark-read は表示したメッセージを既読にします。" + "value": "جاري إعادة تسمية %@…" } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "사용법: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\n에이전트 메시지를 최신순으로 나열합니다. --surface가 없으면\n모든 표면의 메시지를 나열합니다. --mark-read는 나열된 메시지를 읽음으로 표시합니다." + "value": "Renombrando %@…" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\n列出代理消息,最新的在前。未指定 --surface 时,列出\n所有表面的消息。--mark-read 会将列出的消息标记为已读。" + "value": "正在重新命名 %@…" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "用法: cmux agent inbox [--surface ] [--state queued|delivered|read] [--limit ] [--mark-read] [--json]\n\n列出代理訊息,最新的在前。未指定 --surface 時,列出\n所有表面的訊息。--mark-read 會將列出的訊息標為已讀。" + "value": "%@ 이름 바꾸는 중…" } } } }, - "cli.help.agentMessage": { + "cloudTree.operation.clearName": { "extractionState": "manual", "localizations": { - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nيرسل رسالة إلى الوكيل الذي يعمل في مساحة عمل أو سطح آخر. يوصلها cmux\nعبر خطافات ذلك الوكيل، ولا يكتبها أبدًا في طرفيته،\nلذا لا يمكن أن تقع داخل موجّه مكتوب نصفه. تستيقظ جلسة Claude Code الخاملة\nلقراءتها، وتقرأها الجلسة المشغولة في خطوتها التالية.\n\n هو معرّف أو مرجع مساحة عمل أو سطح (workspace:2، surface:5)، أو\nعنوان مساحة عمل (مطابقة تامة أولًا، ثم بادئة فريدة). استخدم - لقراءة النص من\nstdin. القيمة الافتراضية لـ --from هي عنوان مساحة العمل المرسِلة.\n\nأمثلة:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "Clearing %@…" } }, - "de": { + "ja": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nSendet eine Nachricht an den Agenten in einem anderen Workspace oder einer anderen Oberfläche. cmux\nliefert sie über die Hooks dieses Agenten aus, nie durch Tippen in sein Terminal,\ndaher kann sie nicht in einem halb getippten Prompt landen. Eine untätige Claude-Code-Sitzung wacht\nauf, um sie zu lesen; eine beschäftigte liest sie bei ihrem nächsten Schritt.\n\n ist eine Workspace- oder Oberflächen-ID bzw. -Referenz (workspace:2, surface:5) oder ein\nWorkspace-Titel (zuerst exakt, dann ein eindeutiges Präfix). Mit - wird der Text von\nstdin gelesen. --from ist standardmäßig der Titel des sendenden Workspace.\n\nBeispiele:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "%@ のカスタム名をクリアしています…" } }, - "en": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Usage: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nSend a message to the agent running in another workspace or surface. cmux\ndelivers it through that agent's hooks, never by typing into its terminal,\nso it cannot land in a half-typed prompt. An idle Claude Code session wakes\nup to read it; a busy one reads it at its next step.\n\n is a workspace or surface id or ref (workspace:2, surface:5), or a\nworkspace title (exact, then a unique prefix). Use - to read the text from\nstdin. --from defaults to the sending workspace's title.\n\nExamples:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "正在清空 %@…" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Uso: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nEnvía un mensaje al agente que se ejecuta en otro espacio de trabajo o superficie. cmux\nlo entrega a través de los hooks de ese agente, nunca escribiendo en su terminal,\npor lo que no puede acabar en un prompt a medio escribir. Una sesión inactiva de Claude Code se despierta\npara leerlo; una ocupada lo lee en su siguiente paso.\n\n es un ID o referencia de espacio de trabajo o superficie (workspace:2, surface:5), o un\ntítulo de espacio de trabajo (exacto, luego un prefijo único). Usa - para leer el texto desde\nstdin. --from usa por defecto el título del espacio de trabajo que envía.\n\nEjemplos:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "%@ wird geleert…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\nEnvoie un message à l'agent d'un autre espace de travail ou d'une autre surface. cmux\nle remet via les hooks de cet agent, jamais en tapant dans son terminal,\nil ne peut donc pas atterrir dans un prompt à moitié tapé. Une session Claude Code inactive se réveille\npour le lire ; une session occupée le lit à sa prochaine étape.\n\n est un identifiant ou une référence d'espace de travail ou de surface (workspace:2, surface:5), ou un\ntitre d'espace de travail (exact, puis un préfixe unique). Utilisez - pour lire le texte depuis\nstdin. Par défaut, --from vaut le titre de l'espace de travail expéditeur.\n\nExemples :\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "Effacement de %@…" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "使い方: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n別のワークスペースまたはサーフェスで動いているエージェントにメッセージを送ります。cmux は\nそのエージェントのフック経由で届け、ターミナルに打ち込むことはないので、\n入力途中のプロンプトに紛れ込みません。アイドル中の Claude Code セッションは起きて\n読み、作業中のセッションは次のステップで読みます。\n\n はワークスペースかサーフェスの ID または参照(workspace:2、surface:5)、または\nワークスペースのタイトル(完全一致、次に一意な前方一致)です。- を指定すると\nstdin からテキストを読みます。--from の既定値は送信元ワークスペースのタイトルです。\n\n例:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "جاري مسح %@…" } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "사용법: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n다른 워크스페이스나 표면에서 실행 중인 에이전트에게 메시지를 보냅니다. cmux는\n해당 에이전트의 훅을 통해 전달하며, 절대 터미널에 입력하지 않으므로\n반쯤 입력된 프롬프트에 섞일 수 없습니다. 유휴 상태의 Claude Code 세션은 깨어나서\n읽고, 작업 중인 세션은 다음 단계에서 읽습니다.\n\n은 워크스페이스나 표면의 ID 또는 참조(workspace:2, surface:5), 또는\n워크스페이스 제목(먼저 정확히 일치, 그다음 고유한 접두사)입니다. -를 쓰면\nstdin에서 텍스트를 읽습니다. --from의 기본값은 보내는 워크스페이스의 제목입니다.\n\n예시:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "Limpiando %@…" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n向在另一个工作区或表面中运行的代理发送消息。cmux\n通过该代理的 hook 投递消息,绝不会向它的终端输入按键,\n因此消息不会落入输入到一半的提示中。空闲的 Claude Code 会话会醒来\n阅读它;忙碌的会话会在下一步阅读。\n\n 是工作区或表面的 ID 或引用(workspace:2、surface:5),或\n工作区标题(先完全匹配,再找唯一前缀)。使用 - 从\nstdin 读取文本。--from 默认为发送端工作区的标题。\n\n示例:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "正在清除 %@…" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "用法: cmux agent message [--from ] [--thread ] [--json] [--] \n cmux agent message --reply-to [--from ] [--json] [--] \n\n傳送訊息給在另一個工作區或表面中執行的代理。cmux\n透過該代理的 hook 遞送訊息,絕不會打字到它的終端機,\n因此訊息不會落入打到一半的提示中。閒置的 Claude Code 工作階段會醒來\n閱讀它;忙碌中的工作階段會在下一步閱讀。\n\n 是工作區或表面的 ID 或參照(workspace:2、surface:5),或\n工作區標題(先完全符合,再找唯一前綴)。使用 - 從\nstdin 讀取文字。--from 預設為傳送端工作區的標題。\n\n範例:\n cmux agent message cmux-remote-status \"The relay fix is on main, rebase when free.\"\n cmux agent message --reply-to 3f2a... \"Done, PR is #123.\"" + "value": "%@ 비우는 중…" } } } }, - "socket.agentMessage.error.bodyTooLarge": { + "cloudTree.rename.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The message is too long. The limit is 32 KiB." + "value": "Rename “%@”" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "الرسالة طويلة جدًا. الحد الأقصى 32 كيلوبايت." + "value": "「%@」の名称変更" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Die Nachricht ist zu lang. Die Grenze liegt bei 32 KiB." + "value": "重命名“%@”" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "El mensaje es demasiado largo. El límite es de 32 KiB." + "value": "„%@“ umbenennen" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Le message est trop long. La limite est de 32 Kio." + "value": "Renommer « %@ »" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "メッセージが長すぎます。上限は 32 KiB です。" + "value": "إعادة تسمية “%@”" } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "메시지가 너무 깁니다. 한도는 32KiB입니다." + "value": "Renombrar “%@”" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "消息太长。上限为 32 KiB。" + "value": "重新命名「%@」" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "訊息太長。上限為 32 KiB。" + "value": "“%@” 이름 바꾸기" } } } }, - "socket.agentMessage.error.controlCharacter": { + "cloudTree.error.renameNoRemoteTab": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The message contains control characters. Only text, newlines and tabs are allowed." + "value": "This tab is not open in a remote workspace." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تحتوي الرسالة على أحرف تحكم. لا يُسمح إلا بالنص وفواصل الأسطر وعلامات الجدولة." + "value": "علامة التبويب هذه ليست مفتوحة في مساحة عمل بعيدة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Nachricht enthält Steuerzeichen. Erlaubt sind nur Text, Zeilenumbrüche und Tabulatoren." + "value": "Dieser Tab ist in keinem Remote-Arbeitsbereich geöffnet." } }, "es": { "stringUnit": { "state": "translated", - "value": "El mensaje contiene caracteres de control. Solo se permiten texto, saltos de línea y tabulaciones." + "value": "Esta pestaña no está abierta en un espacio de trabajo remoto." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Le message contient des caractères de contrôle. Seuls le texte, les retours à la ligne et les tabulations sont autorisés." + "value": "Cet onglet n’est pas ouvert dans un espace de travail distant." } }, "ja": { "stringUnit": { "state": "translated", - "value": "メッセージに制御文字が含まれています。使えるのはテキスト、改行、タブだけです。" + "value": "このタブはリモートワークスペースで開かれていません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "메시지에 제어 문자가 있습니다. 텍스트, 줄바꿈, 탭만 허용됩니다." + "value": "이 탭은 원격 작업 공간에 열려 있지 않습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "消息包含控制字符。只允许文本、换行和制表符。" + "value": "此标签页未在任何远程工作区中打开。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "訊息包含控制字元。只允許文字、換行和定位字元。" + "value": "此標籤頁未在遠端工作區中開啟。" } } } }, - "socket.agentMessage.error.emptyBody": { + "cloudTree.error.renameTabUnavailable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The message is empty." + "value": "The current state for tab %@ is unavailable. Refresh and retry before renaming." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "الرسالة فارغة." + "value": "Der aktuelle Zustand von Tab %@ ist nicht verfügbar. Aktualisieren Sie die Ansicht und versuchen Sie die Umbenennung erneut." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Die Nachricht ist leer." + "value": "L’état actuel de l’onglet %@ n’est pas disponible. Actualisez la page et réessayez de le renommer." } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "El mensaje está vacío." + "value": "الحالة الحالية لعلامة التبويب %@ غير متاحة. حدّث الصفحة وحاول إعادة التسمية مرة أخرى." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Le message est vide." + "value": "El estado actual de la pestaña %@ no está disponible. Actualiza la página y vuelve a intentar cambiarle el nombre." } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "メッセージが空です。" + "value": "標籤頁 %@ 的目前狀態無法使用。請重新整理後再試一次重新命名。" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "메시지가 비어 있습니다." + "value": "标签页 %@ 的当前状态不可用。请刷新后重试重命名。" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "消息为空。" + "value": "탭 %@의 현재 상태를 사용할 수 없습니다. 새로 고친 후 이름을 다시 바꾸십시오." } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "訊息是空的。" + "value": "タブ %@ の現在の状態を取得できません。更新してから名前の変更をもう一度お試しください。" } } } }, - "socket.agentMessage.error.invalidSender": { + "cli.send.error.dialogOpen": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The sender name must be one line of at most 64 characters." + "value": "%1$@: %2$@ is waiting on a question or dialog, so nothing was sent. Retry once it is answered, or pass --force to send anyway." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب أن يكون اسم المرسل سطرًا واحدًا لا يتجاوز 64 حرفًا." + "value": "%1$@: ينتظر %2$@ إجابة على سؤال أو مربع حوار، لذا لم يُرسل شيء. أعد المحاولة بعد الإجابة، أو مرّر --force للإرسال على أي حال." } }, "de": { "stringUnit": { "state": "translated", - "value": "Der Absendername muss eine einzelne Zeile mit höchstens 64 Zeichen sein." + "value": "%1$@: %2$@ wartet auf eine Frage oder einen Dialog, daher wurde nichts gesendet. Versuche es erneut, sobald sie beantwortet ist, oder übergib --force, um trotzdem zu senden." } }, "es": { "stringUnit": { "state": "translated", - "value": "El nombre del remitente debe ser una sola línea de 64 caracteres como máximo." + "value": "%1$@: %2$@ está esperando una pregunta o un diálogo, así que no se envió nada. Vuelve a intentarlo cuando se responda, o pasa --force para enviarlo de todos modos." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Le nom de l'expéditeur doit tenir sur une ligne de 64 caractères au maximum." + "value": "%1$@ : %2$@ attend la réponse à une question ou à une boîte de dialogue, rien n'a donc été envoyé. Réessayez une fois la réponse donnée, ou passez --force pour envoyer quand même." } }, "ja": { "stringUnit": { "state": "translated", - "value": "送信者名は 64 文字以内の 1 行にしてください。" + "value": "%1$@: %2$@ は質問またはダイアログへの応答を待っているため、何も送信しませんでした。応答後に再試行するか、--force を指定して送信してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "보낸 사람 이름은 64자 이하의 한 줄이어야 합니다." + "value": "%1$@: %2$@에서 질문 또는 대화상자가 응답을 기다리고 있어 아무것도 보내지 않았습니다. 응답한 후 다시 시도하거나 --force를 지정해 그래도 보내세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "发送者名称必须是一行,最多 64 个字符。" + "value": "%1$@:%2$@ 正在等待问题或对话框的回应,因此未发送任何内容。请在回应后重试,或传入 --force 强制发送。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "寄件者名稱必須是一行,最多 64 個字元。" + "value": "%1$@:%2$@ 正在等待問題或對話框的回應,因此未傳送任何內容。請在回應後重試,或傳入 --force 強制傳送。" } } } }, - "socket.agentMessage.error.invalidState": { + "cli.send.error.draftPresent": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "State must be queued, delivered or read." + "value": "%1$@: the agent prompt in %2$@ holds text someone is typing, so nothing was sent. Retry when the prompt is clear, or pass --force to send anyway." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب أن تكون الحالة queued أو delivered أو read." + "value": "%1$@: تحتوي مطالبة الوكيل في %2$@ على نص يكتبه شخص ما، لذا لم يُرسل شيء. أعد المحاولة عندما تصبح المطالبة فارغة، أو مرّر --force للإرسال على أي حال." } }, "de": { "stringUnit": { "state": "translated", - "value": "Der Status muss queued, delivered oder read sein." + "value": "%1$@: Die Agent-Eingabe in %2$@ enthält Text, den jemand gerade tippt, daher wurde nichts gesendet. Versuche es erneut, wenn die Eingabe leer ist, oder übergib --force, um trotzdem zu senden." } }, "es": { "stringUnit": { "state": "translated", - "value": "El estado debe ser queued, delivered o read." + "value": "%1$@: el prompt del agente en %2$@ contiene texto que alguien está escribiendo, así que no se envió nada. Vuelve a intentarlo cuando el prompt esté vacío, o pasa --force para enviarlo de todos modos." } }, "fr": { "stringUnit": { "state": "translated", - "value": "L'état doit être queued, delivered ou read." + "value": "%1$@ : l'invite de l'agent dans %2$@ contient du texte en cours de saisie, rien n'a donc été envoyé. Réessayez quand l'invite est vide, ou passez --force pour envoyer quand même." } }, "ja": { "stringUnit": { "state": "translated", - "value": "状態は queued、delivered、read のいずれかにしてください。" + "value": "%1$@: %2$@ のエージェントのプロンプトに誰かが入力中のテキストがあるため、何も送信しませんでした。プロンプトが空になってから再試行するか、--force を指定して送信してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "상태는 queued, delivered, read 중 하나여야 합니다." + "value": "%1$@: %2$@의 에이전트 프롬프트에 누군가 입력 중인 텍스트가 있어 아무것도 보내지 않았습니다. 프롬프트가 비면 다시 시도하거나 --force를 지정해 그래도 보내세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "状态必须是 queued、delivered 或 read。" + "value": "%1$@:%2$@ 中的代理提示里有人正在输入文本,因此未发送任何内容。请在提示清空后重试,或传入 --force 强制发送。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "狀態必須是 queued、delivered 或 read。" + "value": "%1$@:%2$@ 中的代理程式提示裡有人正在輸入文字,因此未傳送任何內容。請在提示清空後重試,或傳入 --force 強制傳送。" } } } }, - "socket.agentMessage.error.missingSurface": { + "agent.autoResume.status": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "A surface_id UUID is required." + "value": "Auto-resumed ×%lld" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "يلزم تحديد surface_id بصيغة UUID." + "value": "Automatisch fortgesetzt ×%lld" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Eine surface_id-UUID ist erforderlich." + "value": "Reprise automatique ×%lld" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Se requiere un UUID en surface_id." + "value": "استئناف تلقائي ×%lld" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Un UUID surface_id est requis." + "value": "Reanudado automáticamente ×%lld" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "surface_id の UUID が必要です。" + "value": "已自動繼續 ×%lld" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "surface_id UUID가 필요합니다." + "value": "已自动继续 ×%lld" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "需要 surface_id UUID。" + "value": "자동 재개 ×%lld" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "需要 surface_id UUID。" + "value": "自動再開 ×%lld" } } } }, - "socket.agentMessage.error.missingTarget": { + "settings.automation.agentAutoResume": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Give a target workspace or surface, or a message id to reply to." + "value": "Auto-Resume Agents After Errors" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "حدّد مساحة عمل أو سطحًا هدفًا، أو معرّف رسالة للرد عليها." + "value": "Agenten nach Fehlern automatisch fortsetzen" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Gib einen Ziel-Workspace oder eine Zieloberfläche an, oder eine Nachrichten-ID, auf die geantwortet werden soll." + "value": "Relancer automatiquement les agents après une erreur" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Indica un espacio de trabajo o una superficie de destino, o el ID de un mensaje al que responder." + "value": "استئناف الوكلاء تلقائيًا بعد الأخطاء" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Indiquez un espace de travail ou une surface cible, ou l'identifiant du message auquel répondre." + "value": "Reanudar agentes automáticamente tras errores" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "宛先のワークスペースかサーフェス、または返信するメッセージの ID を指定してください。" + "value": "錯誤後自動恢復代理" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "대상 워크스페이스나 표면, 또는 답장할 메시지 ID를 지정하세요." + "value": "出错后自动恢复代理" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "请指定目标工作区或表面,或要回复的消息 ID。" + "value": "오류 후 에이전트 자동 재개" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "請指定目標工作區或表面,或要回覆的訊息 ID。" + "value": "エラー後にエージェントを自動再開" } } } }, - "socket.agentMessage.error.noReplyAddress": { + "settings.automation.agentAutoResume.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "That message has no sender surface to reply to." + "value": "Send “continue” when an agent's turn ends on a retryable error such as model capacity or a dropped connection." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "لا يوجد لهذه الرسالة سطح مرسل يمكن الرد عليه." + "value": "Sendet „continue“, wenn der Zug eines Agenten mit einem wiederholbaren Fehler endet, etwa ausgelasteter Modellkapazität oder einer getrennten Verbindung." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Diese Nachricht hat keine Absenderoberfläche, an die geantwortet werden kann." + "value": "Envoie « continue » quand le tour d’un agent se termine sur une erreur réessayable, comme un modèle saturé ou une connexion interrompue." } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Ese mensaje no tiene una superficie de remitente a la que responder." + "value": "يرسل \"continue\" عندما ينتهي دور الوكيل بخطأ يمكن إعادة المحاولة بعده، مثل امتلاء سعة النموذج أو انقطاع الاتصال." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Ce message n'a pas de surface d'expéditeur à laquelle répondre." + "value": "Envía «continue» cuando el turno de un agente termina con un error reintentable, como falta de capacidad del modelo o una conexión perdida." } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "このメッセージには返信先となる送信元サーフェスがありません。" + "value": "當代理的回合因可重試的錯誤(例如模型容量不足或連線中斷)而結束時,傳送「continue」。" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "이 메시지에는 답장할 보낸 사람 표면이 없습니다." + "value": "当代理的回合因可重试的错误(例如模型容量不足或连接中断)而结束时,发送“continue”。" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "该消息没有可回复的发送者表面。" + "value": "모델 용량 초과나 연결 끊김처럼 재시도 가능한 오류로 에이전트의 턴이 끝나면 “continue”를 보냅니다." } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "該訊息沒有可回覆的寄件者表面。" + "value": "モデルの容量不足や接続切断など、再試行可能なエラーでエージェントのターンが終了したときに「continue」を送信します。" } } } }, - "socket.agentMessage.error.notSaved": { + "settings.search.alias.setting.automation.agent-error-auto-resume": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The message was not sent: cmux could not save it to the message journal." + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "لم يتم إرسال الرسالة: تعذّر على cmux حفظها في سجل الرسائل." + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error automatisch fortsetzen fortfahren wiederholen kapazität überlastet agent fehler" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Die Nachricht wurde nicht gesendet: cmux konnte sie nicht im Nachrichtenjournal speichern." + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error reprise automatique continuer réessayer capacité surchargé agent erreur" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "El mensaje no se envió: cmux no pudo guardarlo en el registro de mensajes." + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error استئناف تلقائي متابعة إعادة المحاولة سعة حمل زائد وكيل خطأ" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Le message n’a pas été envoyé : cmux n’a pas pu l’enregistrer dans le journal des messages." + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error reanudar automáticamente continuar reintentar capacidad sobrecargado agente error" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "メッセージは送信されませんでした: cmux がメッセージジャーナルに保存できませんでした。" + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 自動恢復 繼續 重試 容量 過載 代理 錯誤" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "메시지를 보내지 못했습니다: cmux가 메시지 저널에 저장하지 못했습니다." + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 自动恢复 继续 重试 容量 过载 代理 错误" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "消息未发送:cmux 无法将其保存到消息日志。" + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 자동 재개 계속 재시도 용량 과부하 에이전트 오류" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "訊息未傳送:cmux 無法將其儲存到訊息日誌。" + "value": "automation.agentAutoResume auto resume continue retry capacity overloaded agent error 自動再開 続行 再試行 容量 過負荷 エージェント エラー" } } } }, - "socket.agentMessage.error.targetNotFound": { + "settings.textBox.betaWarning": { "extractionState": "manual", "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "No workspace or surface matches that target." - } - }, "ar": { "stringUnit": { "state": "translated", - "value": "لا توجد مساحة عمل أو سطح يطابق هذا الهدف." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kein Workspace und keine Oberfläche passt zu diesem Ziel." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Ningún espacio de trabajo ni superficie coincide con ese destino." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Aucun espace de travail ni aucune surface ne correspond à cette cible." + "value": "مربع النص ميزة تجريبية. قد تتغير إعداداته الافتراضية وسلوكه أثناء اختباره." } }, - "ja": { + "bs": { "stringUnit": { - "state": "translated", - "value": "その宛先に一致するワークスペースもサーフェスもありません。" + "state": "needs_review", + "value": "TextBox je beta funkcija. Zadane vrijednosti i ponašanje mogu se promijeniti tokom testiranja." } }, - "ko": { + "da": { "stringUnit": { - "state": "translated", - "value": "해당 대상과 일치하는 워크스페이스나 표면이 없습니다." + "state": "needs_review", + "value": "TextBox er en beta-funktion. Standarder og adfærd kan ændre sig under test." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "没有与该目标匹配的工作区或表面。" + "value": "TextBox ist eine Beta-Funktion. Standardwerte und Verhalten können sich während der Testphase ändern." } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "沒有符合該目標的工作區或表面。" - } - } - } - }, - "socket.agentMessage.error.unknownMessage": { - "extractionState": "manual", - "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No message with id %@." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لا توجد رسالة بالمعرّف %@." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Keine Nachricht mit der ID %@." + "value": "TextBox is a beta feature. Its defaults and behavior may change while it is being tested." } }, "es": { "stringUnit": { "state": "translated", - "value": "No hay ningún mensaje con el ID %@." + "value": "TextBox es una característica beta. Sus valores predeterminados y comportamiento pueden cambiar mientras se prueba." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Aucun message avec l'identifiant %@." + "value": "La TextBox est une fonctionnalité bêta. Ses valeurs par défaut et son comportement peuvent changer pendant sa phase de test." } }, - "ja": { + "it": { "stringUnit": { - "state": "translated", - "value": "ID が %@ のメッセージはありません。" + "state": "needs_review", + "value": "TextBox è una funzionalità beta. Valori predefiniti e comportamento possono cambiare durante i test." } }, - "ko": { + "ja": { "stringUnit": { "state": "translated", - "value": "ID가 %@인 메시지가 없습니다." + "value": "TextBox はベータ機能です。テスト中のため、既定値や動作が変わる場合があります。" } }, - "zh-Hans": { + "km": { "stringUnit": { - "state": "translated", - "value": "没有 ID 为 %@ 的消息。" + "state": "needs_review", + "value": "TextBox គឺជាមុខងារបេតា។ តម្លៃលំនាំដើម និងឥរិយាបថអាចផ្លាស់ប្តូរនៅពេលកំពុងសាកល្បង។" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "沒有 ID 為 %@ 的訊息。" + "value": "TextBox는 베타 기능입니다. 테스트되는 동안 기본값 및 동작이 변경될 수 있습니다." } - } - } - }, - "agentHibernation.manual.agentBusy": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "nb": { "stringUnit": { - "state": "translated", - "value": "This agent is working or waiting for input." + "state": "needs_review", + "value": "TextBox er en betafunksjon. Standardverdier og oppførsel kan endres mens den testes." } }, - "ja": { + "pl": { "stringUnit": { - "state": "translated", - "value": "このエージェントは作業中か、入力を待っています。" + "state": "needs_review", + "value": "TextBox jest funkcją beta. Domyślne ustawienia i zachowanie mogą się zmienić podczas testów." } }, - "de": { + "pt-BR": { "stringUnit": { - "state": "translated", - "value": "Dieser Agent arbeitet oder wartet auf eine Eingabe." + "state": "needs_review", + "value": "TextBox é um recurso beta. Seus padrões e comportamento podem mudar durante os testes." } }, - "fr": { + "ru": { "stringUnit": { - "state": "translated", - "value": "Cet agent travaille ou attend une saisie." + "state": "needs_review", + "value": "TextBox — бета-функция. Во время тестирования значения по умолчанию и поведение могут измениться." } }, - "ar": { + "th": { "stringUnit": { - "state": "translated", - "value": "هذا الوكيل يعمل أو ينتظر إدخالًا." + "state": "needs_review", + "value": "TextBox เป็นฟีเจอร์เบต้า ค่าเริ่มต้นและพฤติกรรมอาจเปลี่ยนแปลงระหว่างการทดสอบ" } }, - "es": { + "tr": { "stringUnit": { - "state": "translated", - "value": "Este agente está trabajando o esperando una respuesta." + "state": "needs_review", + "value": "TextBox bir beta özelliğidir. Varsayılanlar ve davranış test süresince değişebilir." } }, - "zh-Hant": { + "uk": { "stringUnit": { - "state": "translated", - "value": "此代理程式正在工作或等待輸入。" + "state": "needs_review", + "value": "TextBox — бета-функція. Під час тестування типові значення й поведінка можуть змінюватися." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体正在工作或等待输入。" + "value": "TextBox 是测试版功能。在测试期间,默认值和行为可能会变化。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트는 작업 중이거나 입력을 기다리고 있습니다." + "value": "文字框是一項 Beta 功能。在測試期間,其預設值與行為可能會發生變更。" } } } }, - "agentHibernation.manual.alreadyHibernated": { + "cli.auth.team.inviteFailed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent is already hibernated." + "value": "Could not invite %1$@: %2$@" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントはすでに休止中です。" + "value": "تعذّرت دعوة %1$@: %2$@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent ruht bereits." + "value": "%1$@ konnte nicht eingeladen werden: %2$@" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent est déjà en veille." + "value": "No se pudo invitar a %1$@: %2$@" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "هذا الوكيل في وضع السبات بالفعل." + "value": "Impossible d’inviter %1$@ : %2$@" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente ya está en hibernación." + "value": "%1$@ を招待できませんでした: %2$@" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式已經休眠。" + "value": "%1$@을(를) 초대할 수 없습니다: %2$@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体已休眠。" + "value": "无法邀请 %1$@:%2$@" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트는 이미 휴면 중입니다." + "value": "無法邀請 %1$@:%2$@" } } } }, - "agentHibernation.manual.lifecycleUnknown": { + "cli.auth.team.inviteUsage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux can't tell whether this agent is idle, so it was left running." + "value": "Usage: cmux auth team invite ... [--role admin|member] [--team ]" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux はこのエージェントがアイドル状態か判断できないため、実行したままにしました。" + "value": "الاستخدام: cmux auth team invite ... [--role admin|member] [--team ]" } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux kann nicht erkennen, ob dieser Agent untätig ist, daher läuft er weiter." + "value": "Verwendung: cmux auth team invite ... [--role admin|member] [--team ]" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux ne peut pas savoir si cet agent est inactif, il continue donc de tourner." + "value": "Uso: cmux auth team invite ... [--role admin|member] [--team ]" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "لا يستطيع cmux معرفة ما إذا كان هذا الوكيل خاملًا، لذا تُرك قيد التشغيل." + "value": "Utilisation : cmux auth team invite ... [--role admin|member] [--team ]" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "cmux no puede saber si este agente está inactivo, así que sigue en ejecución." + "value": "使用方法: cmux auth team invite ... [--role admin|member] [--team ]" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux 無法判斷此代理程式是否閒置,因此保持執行。" + "value": "사용법: cmux auth team invite ... [--role admin|member] [--team ]" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 无法判断此智能体是否空闲,因此保持运行。" + "value": "用法:cmux auth team invite ... [--role admin|member] [--team ]" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux가 이 에이전트의 유휴 여부를 알 수 없어 실행 상태로 두었습니다." + "value": "用法:cmux auth team invite ... [--role admin|member] [--team ]" } } } }, - "agentHibernation.manual.notRestorable": { + "cli.auth.team.invited": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This terminal has no agent that cmux can resume the way it was started." + "value": "Invited %@" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このターミナルには、起動時と同じ方法で cmux が再開できるエージェントがありません。" + "value": "تمت دعوة %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieses Terminal hat keinen Agenten, den cmux so fortsetzen kann, wie er gestartet wurde." + "value": "%@ eingeladen" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Ce terminal n’a aucun agent que cmux peut reprendre de la façon dont il a été lancé." + "value": "Se invitó a %@" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "لا تحتوي هذه الطرفية على وكيل يستطيع cmux استئنافه بالطريقة التي بدأ بها." + "value": "%@ invité" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Esta terminal no tiene un agente que cmux pueda reanudar tal como se inició." + "value": "%@ を招待しました" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此終端機沒有 cmux 能以原本啟動方式恢復的代理程式。" + "value": "%@을(를) 초대했습니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此终端没有 cmux 能按原启动方式恢复的智能体。" + "value": "已邀请 %@" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 터미널에는 시작된 방식 그대로 cmux가 재개할 수 있는 에이전트가 없습니다." + "value": "已邀請 %@" } } } }, - "agentHibernation.manual.notRunning": { + "cli.auth.team.pendingInvitation": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This terminal is not running, so there is nothing to hibernate." + "value": " pending %1$@ %2$@ %3$@" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このターミナルは実行されていないため、休止するものがありません。" + "value": " معلّقة %1$@ %2$@ %3$@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieses Terminal läuft nicht, daher gibt es nichts zum Ruhen." + "value": " ausstehend %1$@ %2$@ %3$@" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Ce terminal n’est pas en cours d’exécution, il n’y a donc rien à mettre en veille." + "value": " pendiente %1$@ %2$@ %3$@" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "هذه الطرفية لا تعمل، لذا لا يوجد ما يمكن وضعه في السبات." + "value": " en attente %1$@ %2$@ %3$@" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Esta terminal no se está ejecutando, así que no hay nada que hibernar." + "value": " 保留中 %1$@ %2$@ %3$@" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此終端機未在執行,沒有可休眠的內容。" + "value": " 대기 중 %1$@ %2$@ %3$@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此终端未在运行,没有可休眠的内容。" + "value": " 待处理 %1$@ %2$@ %3$@" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 터미널은 실행 중이 아니어서 휴면할 대상이 없습니다." + "value": " 待處理 %1$@ %2$@ %3$@" } } } }, - "agentHibernation.manual.processScopeUnsafe": { + "cli.auth.team.removeUsage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This terminal is running other work, such as background jobs, that hibernation would stop." + "value": "Usage: cmux auth team remove [--team ]" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このターミナルでは、休止すると停止してしまうバックグラウンドジョブなどの作業が実行中です。" + "value": "الاستخدام: cmux auth team remove [--team ]" } }, "de": { "stringUnit": { "state": "translated", - "value": "In diesem Terminal läuft weitere Arbeit, etwa Hintergrundjobs, die das Ruhen beenden würde." + "value": "Verwendung: cmux auth team remove [--team ]" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Ce terminal exécute d’autres tâches, comme des tâches en arrière-plan, que la mise en veille arrêterait." + "value": "Uso: cmux auth team remove [--team ]" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "تشغّل هذه الطرفية أعمالًا أخرى، مثل مهام الخلفية، سيوقفها السبات." + "value": "Utilisation : cmux auth team remove [--team ]" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Esta terminal está ejecutando otro trabajo, como tareas en segundo plano, que la hibernación detendría." + "value": "使用方法: cmux auth team remove [--team ]" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此終端機正在執行其他工作(例如背景工作),休眠會使其停止。" + "value": "사용법: cmux auth team remove [--team ]" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此终端正在运行其他工作(例如后台任务),休眠会使其停止。" + "value": "用法:cmux auth team remove [--team ]" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 터미널에서 백그라운드 작업 등 휴면하면 중단될 다른 작업이 실행 중입니다." + "value": "用法:cmux auth team remove [--team ]" } } } }, - "agentHibernation.manual.recentlyActive": { + "cli.auth.team.removed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent was active a moment ago. Try again in a few seconds." + "value": "Removed." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントは少し前まで動作していました。数秒後にもう一度お試しください。" + "value": "تمت الإزالة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent war gerade eben aktiv. Versuche es in ein paar Sekunden erneut." + "value": "Entfernt." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent était actif il y a un instant. Réessayez dans quelques secondes." + "value": "Eliminado." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "كان هذا الوكيل نشطًا قبل لحظة. حاول مرة أخرى بعد بضع ثوانٍ." + "value": "Supprimé." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente estuvo activo hace un momento. Inténtalo de nuevo en unos segundos." + "value": "削除しました。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式剛剛仍在活動。請幾秒後再試。" + "value": "제거했습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体刚刚仍在活动。请几秒后再试。" + "value": "已移除。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트는 방금 전까지 활동했습니다. 몇 초 후 다시 시도하세요." + "value": "已移除。" } } } }, - "agentHibernation.manual.surfaceNotFound": { + "cli.auth.team.revokeUsage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No terminal matches that surface." + "value": "Usage: cmux auth team revoke-invite [--team ]" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "そのサーフェスに一致するターミナルはありません。" + "value": "الاستخدام: cmux auth team revoke-invite [--team ]" } }, "de": { "stringUnit": { "state": "translated", - "value": "Kein Terminal passt zu dieser Oberfläche." + "value": "Verwendung: cmux auth team revoke-invite [--team ]" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Aucun terminal ne correspond à cette surface." + "value": "Uso: cmux auth team revoke-invite [--team ]" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "لا توجد طرفية تطابق هذا السطح." + "value": "Utilisation : cmux auth team revoke-invite [--team ]" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Ninguna terminal coincide con esa superficie." + "value": "使用方法: cmux auth team revoke-invite [--team ]" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "沒有符合該 surface 的終端機。" + "value": "사용법: cmux auth team revoke-invite [--team ]" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "没有与该 surface 匹配的终端。" + "value": "用法:cmux auth team revoke-invite [--team ]" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "해당 서피스와 일치하는 터미널이 없습니다." + "value": "用法:cmux auth team revoke-invite [--team ]" } } } }, - "agentHibernation.manual.teardownInProgress": { + "cli.auth.team.revoked": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent is already being hibernated." + "value": "Revoked." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントはすでに休止処理中です。" + "value": "تم الإلغاء." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent wird bereits in den Ruhezustand versetzt." + "value": "Widerrufen." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent est déjà en cours de mise en veille." + "value": "Revocado." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "يجري بالفعل وضع هذا الوكيل في السبات." + "value": "Révoqué." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente ya se está hibernando." + "value": "取り消しました。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式正在休眠中。" + "value": "취소했습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体正在休眠中。" + "value": "已撤销。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트는 이미 휴면 처리 중입니다." + "value": "已撤銷。" } } } }, - "agentHibernation.manual.teardownRefused": { + "teamMembers.error.forbidden": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The agent changed or its session could not be protected, so it was left running. Try again later." + "value": "Only team admins can do that." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "エージェントが変化したか、セッションを保護できなかったため、実行したままにしました。後でもう一度お試しください。" + "value": "يمكن لمسؤولي الفريق فقط القيام بذلك." } }, "de": { "stringUnit": { "state": "translated", - "value": "Der Agent hat sich geändert oder seine Sitzung konnte nicht geschützt werden, daher läuft er weiter. Versuche es später erneut." + "value": "Das können nur Team-Admins." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "L’agent a changé ou sa session n’a pas pu être protégée, il continue donc de tourner. Réessayez plus tard." + "value": "Solo los administradores del equipo pueden hacer eso." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "تغيّر الوكيل أو تعذّرت حماية جلسته، لذا تُرك قيد التشغيل. حاول مرة أخرى لاحقًا." + "value": "Seuls les administrateurs de l’équipe peuvent faire cela." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "El agente cambió o no se pudo proteger su sesión, así que sigue en ejecución. Inténtalo más tarde." + "value": "この操作はチームの管理者のみ行えます。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "代理程式有變動或其工作階段無法受保護,因此保持執行。請稍後再試。" + "value": "팀 관리자만 할 수 있습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "智能体有变化或其会话无法受到保护,因此保持运行。请稍后再试。" + "value": "只有团队管理员可以执行此操作。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "에이전트가 변경되었거나 세션을 보호할 수 없어 실행 상태로 두었습니다. 나중에 다시 시도하세요." + "value": "只有團隊管理員可以執行此操作。" } } } }, - "agentHibernation.manual.unconfirmedInput": { + "teamMembers.error.invalidEmail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent has typed input it has not received yet." + "value": "Enter at least one email address." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントには、まだ受け取っていない入力があります。" + "value": "أدخل عنوان بريد إلكتروني واحدًا على الأقل." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent hat getippte Eingaben, die er noch nicht erhalten hat." + "value": "Gib mindestens eine E-Mail-Adresse ein." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent a une saisie qu’il n’a pas encore reçue." + "value": "Introduce al menos una dirección de correo." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "لدى هذا الوكيل إدخال مكتوب لم يستلمه بعد." + "value": "Saisissez au moins une adresse e-mail." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente tiene texto escrito que aún no ha recibido." + "value": "メールアドレスを 1 つ以上入力してください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式有尚未收到的輸入。" + "value": "이메일 주소를 하나 이상 입력하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体有尚未收到的输入。" + "value": "请输入至少一个电子邮件地址。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트에는 아직 받지 않은 입력이 있습니다." + "value": "請輸入至少一個電子郵件地址。" } } } }, - "agentHibernation.manual.visible": { + "teamMembers.error.invalidLinkOptions": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent is on screen. Switch away from it first." + "value": "Link expiry must be 1, 7 or 30 days and max uses at least 1." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントは画面に表示されています。先に別の場所に切り替えてください。" + "value": "يجب أن تكون صلاحية الرابط 1 أو 7 أو 30 يومًا وعدد الاستخدامات 1 على الأقل." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent ist auf dem Bildschirm. Wechsle zuerst weg." + "value": "Der Link muss nach 1, 7 oder 30 Tagen ablaufen und mindestens 1 Verwendung erlauben." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent est à l’écran. Passez d’abord à autre chose." + "value": "El enlace debe caducar en 1, 7 o 30 días y permitir al menos 1 uso." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "هذا الوكيل ظاهر على الشاشة. انتقل بعيدًا عنه أولًا." + "value": "L’expiration du lien doit être de 1, 7 ou 30 jours et le nombre d’utilisations d’au moins 1." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente está en pantalla. Cambia a otro lugar primero." + "value": "リンクの有効期限は 1、7、30 日のいずれか、最大使用回数は 1 以上にしてください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式正顯示在螢幕上。請先切換離開。" + "value": "링크 만료는 1, 7 또는 30일이어야 하며 최대 사용 횟수는 1 이상이어야 합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体正显示在屏幕上。请先切换离开。" + "value": "链接有效期必须为 1、7 或 30 天,最大使用次数至少为 1。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트가 화면에 표시되어 있습니다. 먼저 다른 곳으로 전환하세요." + "value": "連結有效期必須為 1、7 或 30 天,最大使用次數至少為 1。" } } } }, - "agentHibernation.wake.notHibernated": { + "teamMembers.error.invitationNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent is not hibernated." + "value": "That invitation no longer exists." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントは休止していません。" + "value": "لم تعد هذه الدعوة موجودة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent ruht nicht." + "value": "Diese Einladung existiert nicht mehr." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent n’est pas en veille." + "value": "Esa invitación ya no existe." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "هذا الوكيل ليس في وضع السبات." + "value": "Cette invitation n’existe plus." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente no está en hibernación." + "value": "その招待はもう存在しません。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式未休眠。" + "value": "해당 초대가 더 이상 존재하지 않습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体未休眠。" + "value": "该邀请已不存在。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트는 휴면 중이 아닙니다." + "value": "該邀請已不存在。" } } } }, - "agentHibernation.wake.resumeUnavailable": { + "teamMembers.error.lastAdmin": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux could not start this agent's resume command." + "value": "A team must keep at least one admin." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux はこのエージェントの再開コマンドを開始できませんでした。" + "value": "يجب أن يبقى في الفريق مسؤول واحد على الأقل." } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux konnte den Fortsetzungsbefehl dieses Agenten nicht starten." + "value": "Ein Team braucht mindestens einen Admin." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux n’a pas pu lancer la commande de reprise de cet agent." + "value": "Un equipo debe conservar al menos un administrador." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "تعذّر على cmux بدء أمر الاستئناف لهذا الوكيل." + "value": "Une équipe doit conserver au moins un administrateur." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "cmux no pudo iniciar el comando de reanudación de este agente." + "value": "チームには管理者が少なくとも 1 人必要です。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux 無法啟動此代理程式的恢復指令。" + "value": "팀에는 관리자가 한 명 이상 있어야 합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 无法启动此智能体的恢复命令。" + "value": "团队必须至少保留一位管理员。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux가 이 에이전트의 재개 명령을 시작할 수 없습니다." + "value": "團隊必須至少保留一位管理員。" } } } }, - "agentHibernation.wake.surfaceNotFound": { + "teamMembers.error.memberNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No terminal matches that surface." + "value": "That person is not a member of this team." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "そのサーフェスに一致するターミナルはありません。" + "value": "هذا الشخص ليس عضوًا في هذا الفريق." } }, "de": { "stringUnit": { "state": "translated", - "value": "Kein Terminal passt zu dieser Oberfläche." + "value": "Diese Person ist kein Mitglied dieses Teams." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Aucun terminal ne correspond à cette surface." + "value": "Esa persona no es miembro de este equipo." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "لا توجد طرفية تطابق هذا السطح." + "value": "Cette personne n’est pas membre de cette équipe." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Ninguna terminal coincide con esa superficie." + "value": "その人はこのチームのメンバーではありません。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "沒有符合該 surface 的終端機。" + "value": "해당 사용자는 이 팀의 구성원이 아닙니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "没有与该 surface 匹配的终端。" + "value": "该用户不是此团队的成员。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "해당 서피스와 일치하는 터미널이 없습니다." + "value": "該使用者不是此團隊的成員。" } } } }, - "agentHibernation.wake.teardownInProgress": { + "teamMembers.error.noTeam": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This agent is still being hibernated. Try again in a moment." + "value": "Select a team first." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このエージェントはまだ休止処理中です。しばらくしてからもう一度お試しください。" + "value": "اختر فريقًا أولًا." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Agent wird noch in den Ruhezustand versetzt. Versuche es gleich noch einmal." + "value": "Wähle zuerst ein Team aus." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Cet agent est encore en cours de mise en veille. Réessayez dans un instant." + "value": "Selecciona primero un equipo." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "لا يزال هذا الوكيل قيد الوضع في السبات. حاول مرة أخرى بعد لحظة." + "value": "Sélectionnez d’abord une équipe." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este agente todavía se está hibernando. Inténtalo en un momento." + "value": "まずチームを選択してください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此代理程式仍在休眠中。請稍後再試。" + "value": "먼저 팀을 선택하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此智能体仍在休眠中。请稍后再试。" + "value": "请先选择团队。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 에이전트는 아직 휴면 처리 중입니다. 잠시 후 다시 시도하세요." + "value": "請先選擇團隊。" } } } }, - "cli.agentHibernation.hibernated": { + "teamMembers.error.offline": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "OK hibernated %@" + "value": "cmux Cloud is unreachable. Check your connection and try again." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "OK 休止しました %@" + "value": "تعذّر الوصول إلى cmux Cloud. تحقق من اتصالك وحاول مرة أخرى." } }, "de": { "stringUnit": { "state": "translated", - "value": "OK ruht jetzt %@" + "value": "cmux Cloud ist nicht erreichbar. Prüfe deine Verbindung und versuche es erneut." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "OK mis en veille %@" + "value": "No se puede acceder a cmux Cloud. Comprueba tu conexión e inténtalo de nuevo." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "OK تم وضعه في السبات %@" + "value": "cmux Cloud est injoignable. Vérifiez votre connexion et réessayez." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "OK hibernado %@" + "value": "cmux Cloud に接続できません。接続を確認してもう一度お試しください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "OK 已休眠 %@" + "value": "cmux Cloud에 연결할 수 없습니다. 연결을 확인하고 다시 시도하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "OK 已休眠 %@" + "value": "无法连接到 cmux Cloud。请检查网络连接后重试。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "OK 휴면함 %@" + "value": "無法連線至 cmux Cloud。請檢查網路連線後再試一次。" } } } }, - "cli.agentHibernation.usage": { + "teamMembers.error.rateLimited": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "Too many invitations. Wait a minute and try again." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "使い方: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "دعوات كثيرة جدًا. انتظر دقيقة وحاول مرة أخرى." } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "Zu viele Einladungen. Warte eine Minute und versuche es erneut." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Utilisation: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "Demasiadas invitaciones. Espera un minuto e inténtalo de nuevo." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "Trop d’invitations. Patientez une minute et réessayez." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Uso: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "招待が多すぎます。1 分待ってからもう一度お試しください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "用法: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "초대가 너무 많습니다. 잠시 후 다시 시도하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用法: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "邀请次数过多。请稍候一分钟再试。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "사용법: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" + "value": "邀請次數過多。請稍候一分鐘再試。" } } } }, - "cli.agentHibernation.woke": { + "teamMembers.error.seatLimit": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "OK woke %@" + "value": "This plan includes 3 members. Remove someone or upgrade to Team to invite more." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "OK 再開しました %@" + "value": "تتضمن هذه الخطة 3 أعضاء. أزل أحدهم أو قم بالترقية إلى Team لدعوة المزيد." } }, "de": { "stringUnit": { "state": "translated", - "value": "OK geweckt %@" + "value": "Dieser Plan umfasst 3 Mitglieder. Entferne jemanden oder wechsle zu Team, um weitere einzuladen." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "OK réveillé %@" + "value": "Este plan incluye 3 miembros. Elimina a alguien o cambia a Team para invitar a más." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "OK تم إيقاظه %@" + "value": "Cette offre inclut 3 membres. Retirez quelqu’un ou passez à Team pour inviter plus de personnes." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "OK reanudado %@" + "value": "このプランにはメンバー 3 人が含まれます。誰かを削除するか、Team にアップグレードしてさらに招待してください。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "OK 已喚醒 %@" + "value": "이 플랜에는 구성원 3명이 포함됩니다. 구성원을 제거하거나 Team으로 업그레이드하여 더 초대하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "OK 已唤醒 %@" + "value": "此方案包含 3 位成员。请移除成员或升级到 Team 以邀请更多人。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "OK 깨움 %@" + "value": "此方案包含 3 位成員。請移除成員或升級至 Team 以邀請更多人。" } } } }, - "socket.agentHibernation.surfaceRequired": { + "sidebar.account.invitePeople": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Missing or invalid surface_id" + "value": "Invite people…" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "surface_id がないか無効です" + "value": "دعوة أشخاص…" } }, "de": { "stringUnit": { "state": "translated", - "value": "surface_id fehlt oder ist ungültig" + "value": "Personen einladen…" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "surface_id manquant ou invalide" + "value": "Invitar personas…" } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "surface_id مفقود أو غير صالح" + "value": "Inviter des personnes…" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Falta surface_id o no es válido" + "value": "メンバーを招待…" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "缺少 surface_id 或其無效" + "value": "구성원 초대…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "缺少 surface_id 或其无效" + "value": "邀请成员…" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "surface_id가 없거나 올바르지 않습니다" + "value": "邀請成員…" } } } }, - "settings.textBox.betaWarning": { + "sidebar.account.members": { "extractionState": "manual", "localizations": { - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "مربع النص ميزة تجريبية. قد تتغير إعداداته الافتراضية وسلوكه أثناء اختباره." - } - }, - "bs": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox je beta funkcija. Zadane vrijednosti i ponašanje mogu se promijeniti tokom testiranja." - } - }, - "da": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox er en beta-funktion. Standarder og adfærd kan ændre sig under test." + "value": "Members…" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "TextBox ist eine Beta-Funktion. Standardwerte und Verhalten können sich während der Testphase ändern." + "value": "الأعضاء…" } }, - "en": { + "de": { "stringUnit": { "state": "translated", - "value": "TextBox is a beta feature. Its defaults and behavior may change while it is being tested." + "value": "Mitglieder…" } }, "es": { "stringUnit": { "state": "translated", - "value": "TextBox es una característica beta. Sus valores predeterminados y comportamiento pueden cambiar mientras se prueba." + "value": "Miembros…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "La TextBox est une fonctionnalité bêta. Ses valeurs par défaut et son comportement peuvent changer pendant sa phase de test." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox è una funzionalità beta. Valori predefiniti e comportamento possono cambiare durante i test." + "value": "Membres…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "TextBox はベータ機能です。テスト中のため、既定値や動作が変わる場合があります。" - } - }, - "km": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox គឺជាមុខងារបេតា។ តម្លៃលំនាំដើម និងឥរិយាបថអាចផ្លាស់ប្តូរនៅពេលកំពុងសាកល្បង។" + "value": "メンバー…" } }, "ko": { "stringUnit": { "state": "translated", - "value": "TextBox는 베타 기능입니다. 테스트되는 동안 기본값 및 동작이 변경될 수 있습니다." - } - }, - "nb": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox er en betafunksjon. Standardverdier og oppførsel kan endres mens den testes." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox jest funkcją beta. Domyślne ustawienia i zachowanie mogą się zmienić podczas testów." - } - }, - "pt-BR": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox é um recurso beta. Seus padrões e comportamento podem mudar durante os testes." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox — бета-функция. Во время тестирования значения по умолчанию и поведение могут измениться." - } - }, - "th": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox เป็นฟีเจอร์เบต้า ค่าเริ่มต้นและพฤติกรรมอาจเปลี่ยนแปลงระหว่างการทดสอบ" - } - }, - "tr": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox bir beta özelliğidir. Varsayılanlar ve davranış test süresince değişebilir." - } - }, - "uk": { - "stringUnit": { - "state": "needs_review", - "value": "TextBox — бета-функція. Під час тестування типові значення й поведінка можуть змінюватися." + "value": "구성원…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "TextBox 是测试版功能。在测试期间,默认值和行为可能会变化。" + "value": "成员…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "文字框是一項 Beta 功能。在測試期間,其預設值與行為可能會發生變更。" + "value": "成員…" } } } }, - "cli.auth.team.inviteFailed": { + "command.auth.teamMembers.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Could not invite %1$@: %2$@" + "value": "Invite Team Members" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تعذّرت دعوة %1$@: %2$@" + "value": "دعوة أعضاء الفريق" } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@ konnte nicht eingeladen werden: %2$@" + "value": "Teammitglieder einladen" } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo invitar a %1$@: %2$@" + "value": "Invitar miembros del equipo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Impossible d’inviter %1$@ : %2$@" + "value": "Inviter des membres de l’équipe" } }, "ja": { "stringUnit": { "state": "translated", - "value": "%1$@ を招待できませんでした: %2$@" + "value": "チームメンバーを招待" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@을(를) 초대할 수 없습니다: %2$@" + "value": "팀 구성원 초대" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无法邀请 %1$@:%2$@" + "value": "邀请团队成员" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法邀請 %1$@:%2$@" + "value": "邀請團隊成員" } } } }, - "cli.auth.team.inviteUsage": { + "socket.authTeam.missingInvitation": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux auth team invite ... [--role admin|member] [--team ]" + "value": "An invitation id or link id is required." } }, "ar": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux auth team invite ... [--role admin|member] [--team ]" + "value": "مطلوب معرّف دعوة أو معرّف رابط." } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux auth team invite ... [--role admin|member] [--team ]" + "value": "Eine Einladungs-ID oder Link-ID ist erforderlich." } }, "es": { "stringUnit": { "state": "translated", - "value": "Uso: cmux auth team invite ... [--role admin|member] [--team ]" + "value": "Se requiere un ID de invitación o de enlace." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux auth team invite ... [--role admin|member] [--team ]" + "value": "Un identifiant d’invitation ou de lien est requis." } }, "ja": { "stringUnit": { "state": "translated", - "value": "使用方法: cmux auth team invite ... [--role admin|member] [--team ]" + "value": "招待 ID またはリンク ID が必要です。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "사용법: cmux auth team invite ... [--role admin|member] [--team ]" + "value": "초대 ID 또는 링크 ID가 필요합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team invite ... [--role admin|member] [--team ]" + "value": "需要邀请 ID 或链接 ID。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team invite ... [--role admin|member] [--team ]" + "value": "需要邀請 ID 或連結 ID。" } } } }, - "cli.auth.team.invited": { + "socket.authTeam.missingUser": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invited %@" + "value": "A user id is required." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تمت دعوة %@" + "value": "مطلوب معرّف مستخدم." } }, "de": { "stringUnit": { "state": "translated", - "value": "%@ eingeladen" + "value": "Eine Benutzer-ID ist erforderlich." } }, "es": { "stringUnit": { "state": "translated", - "value": "Se invitó a %@" + "value": "Se requiere un ID de usuario." } }, "fr": { "stringUnit": { "state": "translated", - "value": "%@ invité" + "value": "Un identifiant d’utilisateur est requis." } }, "ja": { "stringUnit": { "state": "translated", - "value": "%@ を招待しました" + "value": "ユーザー ID が必要です。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@을(를) 초대했습니다" + "value": "사용자 ID가 필요합니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已邀请 %@" + "value": "需要用户 ID。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已邀請 %@" + "value": "需要使用者 ID。" } } } }, - "cli.auth.team.pendingInvitation": { + "sidebar.account.invitePeople.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": " pending %1$@ %2$@ %3$@" + "value": "Invite People" } }, "ar": { "stringUnit": { "state": "translated", - "value": " معلّقة %1$@ %2$@ %3$@" + "value": "دعوة أشخاص" } }, "de": { "stringUnit": { "state": "translated", - "value": " ausstehend %1$@ %2$@ %3$@" + "value": "Personen einladen" } }, "es": { "stringUnit": { "state": "translated", - "value": " pendiente %1$@ %2$@ %3$@" + "value": "Invitar personas" } }, "fr": { "stringUnit": { "state": "translated", - "value": " en attente %1$@ %2$@ %3$@" + "value": "Inviter des personnes" } }, "ja": { "stringUnit": { "state": "translated", - "value": " 保留中 %1$@ %2$@ %3$@" + "value": "メンバーを招待" } }, "ko": { "stringUnit": { "state": "translated", - "value": " 대기 중 %1$@ %2$@ %3$@" + "value": "구성원 초대" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": " 待处理 %1$@ %2$@ %3$@" + "value": "邀请成员" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": " 待處理 %1$@ %2$@ %3$@" + "value": "邀請成員" } } } }, - "cli.auth.team.removeUsage": { + "sidebar.account.invite.button": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux auth team remove [--team ]" + "value": "Invite" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux auth team remove [--team ]" + "value": "دعوة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux auth team remove [--team ]" + "value": "Einladen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Uso: cmux auth team remove [--team ]" + "value": "Invitar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux auth team remove [--team ]" + "value": "Inviter" } }, "ja": { "stringUnit": { "state": "translated", - "value": "使用方法: cmux auth team remove [--team ]" + "value": "招待" } }, "ko": { "stringUnit": { "state": "translated", - "value": "사용법: cmux auth team remove [--team ]" + "value": "초대" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team remove [--team ]" + "value": "邀请" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team remove [--team ]" + "value": "邀請" } } } }, - "cli.auth.team.removed": { + "cloudInvite.done": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Removed." + "value": "Done" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تمت الإزالة." + "value": "تم" } }, "de": { "stringUnit": { "state": "translated", - "value": "Entfernt." + "value": "Fertig" } }, "es": { "stringUnit": { "state": "translated", - "value": "Eliminado." + "value": "Listo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Supprimé." + "value": "Terminé" } }, "ja": { "stringUnit": { "state": "translated", - "value": "削除しました。" + "value": "完了" } }, "ko": { "stringUnit": { "state": "translated", - "value": "제거했습니다." + "value": "완료" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已移除。" + "value": "完成" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已移除。" + "value": "完成" } } } }, - "cli.auth.team.revokeUsage": { + "cloudInvite.inviteMore": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux auth team revoke-invite [--team ]" + "value": "Invite More" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux auth team revoke-invite [--team ]" + "value": "دعوة المزيد" } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux auth team revoke-invite [--team ]" + "value": "Weitere einladen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Uso: cmux auth team revoke-invite [--team ]" + "value": "Invitar a más" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux auth team revoke-invite [--team ]" + "value": "Inviter d’autres personnes" } }, "ja": { "stringUnit": { "state": "translated", - "value": "使用方法: cmux auth team revoke-invite [--team ]" + "value": "さらに招待" } }, "ko": { "stringUnit": { "state": "translated", - "value": "사용법: cmux auth team revoke-invite [--team ]" + "value": "더 초대" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team revoke-invite [--team ]" + "value": "继续邀请" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team revoke-invite [--team ]" + "value": "繼續邀請" } } } }, - "cli.auth.team.revoked": { + "cloudInvite.linkCopied": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Revoked." + "value": "Invite link copied. Anyone with it joins as a member for 7 days." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تم الإلغاء." + "value": "تم نسخ رابط الدعوة. يمكن لأي شخص يملكه الانضمام كعضو لمدة 7 أيام." } }, "de": { "stringUnit": { "state": "translated", - "value": "Widerrufen." + "value": "Einladungslink kopiert. Wer ihn hat, tritt 7 Tage lang als Mitglied bei." } }, "es": { "stringUnit": { "state": "translated", - "value": "Revocado." + "value": "Enlace de invitación copiado. Cualquiera que lo tenga se une como miembro durante 7 días." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Révoqué." + "value": "Lien d’invitation copié. Toute personne qui l’a rejoint comme membre pendant 7 jours." } }, "ja": { "stringUnit": { "state": "translated", - "value": "取り消しました。" + "value": "招待リンクをコピーしました。リンクを持つ人は 7 日間メンバーとして参加できます。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "취소했습니다." + "value": "초대 링크를 복사했습니다. 링크가 있으면 7일 동안 구성원으로 참여할 수 있습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已撤销。" + "value": "已复制邀请链接。持有链接者可在 7 天内以成员身份加入。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已撤銷。" + "value": "已複製邀請連結。持有連結者可在 7 天內以成員身分加入。" } } } }, - "teamMembers.error.forbidden": { + "cloudInvite.manageMembers": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Only team admins can do that." + "value": "Manage members…" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يمكن لمسؤولي الفريق فقط القيام بذلك." + "value": "إدارة الأعضاء…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Das können nur Team-Admins." + "value": "Mitglieder verwalten…" } }, "es": { "stringUnit": { "state": "translated", - "value": "Solo los administradores del equipo pueden hacer eso." + "value": "Gestionar miembros…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Seuls les administrateurs de l’équipe peuvent faire cela." + "value": "Gérer les membres…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "この操作はチームの管理者のみ行えます。" + "value": "メンバーを管理…" } }, "ko": { "stringUnit": { "state": "translated", - "value": "팀 관리자만 할 수 있습니다." + "value": "구성원 관리…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "只有团队管理员可以执行此操作。" + "value": "管理成员…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "只有團隊管理員可以執行此操作。" + "value": "管理成員…" } } } }, - "teamMembers.error.invalidEmail": { + "cloudInvite.partial": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Enter at least one email address." + "value": "Could not invite: %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "أدخل عنوان بريد إلكتروني واحدًا على الأقل." + "value": "تعذّرت الدعوة: %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Gib mindestens eine E-Mail-Adresse ein." + "value": "Konnte nicht einladen: %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Introduce al menos una dirección de correo." + "value": "No se pudo invitar: %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Saisissez au moins une adresse e-mail." + "value": "Impossible d’inviter : %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "メールアドレスを 1 つ以上入力してください。" + "value": "招待できませんでした: %@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이메일 주소를 하나 이상 입력하세요." + "value": "초대할 수 없음: %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请输入至少一个电子邮件地址。" + "value": "无法邀请:%@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請輸入至少一個電子郵件地址。" + "value": "無法邀請:%@" } } } }, - "teamMembers.error.invalidLinkOptions": { + "cloudInvite.role.admin": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Link expiry must be 1, 7 or 30 days and max uses at least 1." + "value": "Admin" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب أن تكون صلاحية الرابط 1 أو 7 أو 30 يومًا وعدد الاستخدامات 1 على الأقل." + "value": "مسؤول" } }, "de": { "stringUnit": { "state": "translated", - "value": "Der Link muss nach 1, 7 oder 30 Tagen ablaufen und mindestens 1 Verwendung erlauben." + "value": "Administrator" } }, "es": { "stringUnit": { "state": "translated", - "value": "El enlace debe caducar en 1, 7 o 30 días y permitir al menos 1 uso." + "value": "Administrador" } }, "fr": { "stringUnit": { "state": "translated", - "value": "L’expiration du lien doit être de 1, 7 ou 30 jours et le nombre d’utilisations d’au moins 1." + "value": "Administrateur" } }, "ja": { "stringUnit": { "state": "translated", - "value": "リンクの有効期限は 1、7、30 日のいずれか、最大使用回数は 1 以上にしてください。" + "value": "管理者" } }, "ko": { "stringUnit": { "state": "translated", - "value": "링크 만료는 1, 7 또는 30일이어야 하며 최대 사용 횟수는 1 이상이어야 합니다." + "value": "관리자" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "链接有效期必须为 1、7 或 30 天,最大使用次数至少为 1。" + "value": "管理员" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "連結有效期必須為 1、7 或 30 天,最大使用次數至少為 1。" + "value": "管理員" } } } }, - "teamMembers.error.invitationNotFound": { + "cloudInvite.role.label": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "That invitation no longer exists." + "value": "Join as" } }, "ar": { "stringUnit": { "state": "translated", - "value": "لم تعد هذه الدعوة موجودة." + "value": "الانضمام بصفة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Diese Einladung existiert nicht mehr." + "value": "Beitreten als" } }, "es": { "stringUnit": { "state": "translated", - "value": "Esa invitación ya no existe." + "value": "Unirse como" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Cette invitation n’existe plus." + "value": "Rejoindre en tant que" } }, "ja": { "stringUnit": { "state": "translated", - "value": "その招待はもう存在しません。" + "value": "参加する役割" } }, "ko": { "stringUnit": { "state": "translated", - "value": "해당 초대가 더 이상 존재하지 않습니다." + "value": "참여 역할" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "该邀请已不存在。" + "value": "加入身份" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "該邀請已不存在。" + "value": "加入身分" } } } }, - "teamMembers.error.lastAdmin": { + "cloudInvite.role.member": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "A team must keep at least one admin." + "value": "Member" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب أن يبقى في الفريق مسؤول واحد على الأقل." + "value": "عضو" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ein Team braucht mindestens einen Admin." + "value": "Mitglied" } }, "es": { "stringUnit": { "state": "translated", - "value": "Un equipo debe conservar al menos un administrador." + "value": "Miembro" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Une équipe doit conserver au moins un administrateur." + "value": "Membre" } }, "ja": { "stringUnit": { "state": "translated", - "value": "チームには管理者が少なくとも 1 人必要です。" + "value": "メンバー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "팀에는 관리자가 한 명 이상 있어야 합니다." + "value": "구성원" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "团队必须至少保留一位管理员。" + "value": "成员" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "團隊必須至少保留一位管理員。" + "value": "成員" } } } }, - "teamMembers.error.memberNotFound": { + "cloudInvite.send": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "That person is not a member of this team." + "value": "Send Invite" } }, "ar": { "stringUnit": { "state": "translated", - "value": "هذا الشخص ليس عضوًا في هذا الفريق." + "value": "إرسال الدعوة" } }, "de": { "stringUnit": { "state": "translated", - "value": "Diese Person ist kein Mitglied dieses Teams." + "value": "Einladung senden" } }, "es": { "stringUnit": { "state": "translated", - "value": "Esa persona no es miembro de este equipo." + "value": "Enviar invitación" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Cette personne n’est pas membre de cette équipe." + "value": "Envoyer l’invitation" } }, "ja": { "stringUnit": { "state": "translated", - "value": "その人はこのチームのメンバーではありません。" + "value": "招待を送信" } }, "ko": { "stringUnit": { "state": "translated", - "value": "해당 사용자는 이 팀의 구성원이 아닙니다." + "value": "초대 보내기" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "该用户不是此团队的成员。" + "value": "发送邀请" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "該使用者不是此團隊的成員。" + "value": "傳送邀請" } } } }, - "teamMembers.error.noTeam": { + "cloudInvite.someSent": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Select a team first." + "value": "The other invitations were sent." } }, "ar": { "stringUnit": { "state": "translated", - "value": "اختر فريقًا أولًا." + "value": "تم إرسال الدعوات الأخرى." } }, "de": { "stringUnit": { "state": "translated", - "value": "Wähle zuerst ein Team aus." + "value": "Die übrigen Einladungen wurden gesendet." } }, "es": { "stringUnit": { "state": "translated", - "value": "Selecciona primero un equipo." + "value": "Las demás invitaciones se enviaron." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Sélectionnez d’abord une équipe." + "value": "Les autres invitations ont été envoyées." } }, "ja": { "stringUnit": { "state": "translated", - "value": "まずチームを選択してください。" + "value": "他の招待は送信されました。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "먼저 팀을 선택하세요." + "value": "나머지 초대는 보냈습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请先选择团队。" + "value": "其余邀请已发送。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請先選擇團隊。" + "value": "其餘邀請已送出。" } } } }, - "teamMembers.error.offline": { + "cloudInvite.seatsOpen": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux Cloud is unreachable. Check your connection and try again." + "value": "%1$d of %#@seats@ open on this plan." + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%d seat" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%d seats" + } + } + } + } + } } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "تعذّر الوصول إلى cmux Cloud. تحقق من اتصالك وحاول مرة أخرى." + "value": "%1$d von %#@seats@ dieses Plans frei." + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%d Platz" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%d Plätzen" + } + } + } + } + } } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "cmux Cloud ist nicht erreichbar. Prüfe deine Verbindung und versuche es erneut." + "value": "%1$d sur %#@seats@ libres sur cette offre." + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%d siège" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%d sièges" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%d sièges" + } + } + } + } + } } }, "es": { "stringUnit": { "state": "translated", - "value": "No se puede acceder a cmux Cloud. Comprueba tu conexión e inténtalo de nuevo." + "value": "%1$d de %#@seats@ libres en este plan." + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%d asiento" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%d asientos" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%d asientos" + } + } + } + } + } } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux Cloud est injoignable. Vérifiez votre connexion et réessayez." + "value": "%1$d من %#@seats@ متاحة في هذه الخطة." + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "zero": { + "stringUnit": { + "state": "translated", + "value": "%d مقعد" + } + }, + "one": { + "stringUnit": { + "state": "translated", + "value": "مقعد واحد (%d)" + } + }, + "two": { + "stringUnit": { + "state": "translated", + "value": "مقعدين (%d)" + } + }, + "few": { + "stringUnit": { + "state": "translated", + "value": "%d مقاعد" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%d مقعدًا" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%d مقعد" + } + } + } + } + } } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux Cloud に接続できません。接続を確認してもう一度お試しください。" + "value": "此方案尚有 %1$d / %#@seats@ 可用。" + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%d 個席位" + } + } + } + } + } } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux Cloud에 연결할 수 없습니다. 연결을 확인하고 다시 시도하세요." + "value": "此方案尚有 %1$d / %#@seats@ 可用。" + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%d 个席位" + } + } + } + } + } } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "无法连接到 cmux Cloud。请检查网络连接后重试。" + "value": "이 플랜에서 %1$d / %#@seats@ 남음." + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%d석" + } + } + } + } + } } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "無法連線至 cmux Cloud。請檢查網路連線後再試一次。" + "value": "このプランで %1$d / %#@seats@ が空いています。" + }, + "substitutions": { + "seats": { + "argNum": 2, + "formatSpecifier": "d", + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%d 席" + } + } + } + } + } } } } }, - "teamMembers.error.rateLimited": { + "cloudInvite.copyLink.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Too many invitations. Wait a minute and try again." + "value": "Copy link" } }, "ar": { "stringUnit": { "state": "translated", - "value": "دعوات كثيرة جدًا. انتظر دقيقة وحاول مرة أخرى." + "value": "نسخ الرابط" } }, "de": { "stringUnit": { "state": "translated", - "value": "Zu viele Einladungen. Warte eine Minute und versuche es erneut." + "value": "Link kopieren" } }, "es": { "stringUnit": { "state": "translated", - "value": "Demasiadas invitaciones. Espera un minuto e inténtalo de nuevo." + "value": "Copiar enlace" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Trop d’invitations. Patientez une minute et réessayez." + "value": "Copier le lien" } }, "ja": { "stringUnit": { "state": "translated", - "value": "招待が多すぎます。1 分待ってからもう一度お試しください。" + "value": "リンクをコピー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "초대가 너무 많습니다. 잠시 후 다시 시도하세요." + "value": "링크 복사" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "邀请次数过多。请稍候一分钟再试。" + "value": "复制链接" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "邀請次數過多。請稍候一分鐘再試。" + "value": "複製連結" } } } }, - "teamMembers.error.seatLimit": { + "cloudInvite.emails.placeholder.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This plan includes 3 members. Remove someone or upgrade to Team to invite more." + "value": "Email, comma separated" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تتضمن هذه الخطة 3 أعضاء. أزل أحدهم أو قم بالترقية إلى Team لدعوة المزيد." + "value": "البريد الإلكتروني، مفصول بفواصل" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Plan umfasst 3 Mitglieder. Entferne jemanden oder wechsle zu Team, um weitere einzuladen." + "value": "E-Mail, durch Kommas getrennt" } }, "es": { "stringUnit": { "state": "translated", - "value": "Este plan incluye 3 miembros. Elimina a alguien o cambia a Team para invitar a más." + "value": "Correo, separado por comas" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Cette offre inclut 3 membres. Retirez quelqu’un ou passez à Team pour inviter plus de personnes." + "value": "E-mail, séparés par des virgules" } }, "ja": { "stringUnit": { "state": "translated", - "value": "このプランにはメンバー 3 人が含まれます。誰かを削除するか、Team にアップグレードしてさらに招待してください。" + "value": "メール(カンマ区切り)" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 플랜에는 구성원 3명이 포함됩니다. 구성원을 제거하거나 Team으로 업그레이드하여 더 초대하세요." + "value": "이메일(쉼표로 구분)" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此方案包含 3 位成员。请移除成员或升级到 Team 以邀请更多人。" + "value": "电子邮件,以逗号分隔" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此方案包含 3 位成員。請移除成員或升級至 Team 以邀請更多人。" + "value": "電子郵件,以逗號分隔" } } } }, - "sidebar.account.invitePeople": { + "cloudInvite.heading.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite people…" + "value": "Invite to %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "دعوة أشخاص…" + "value": "دعوة إلى %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Personen einladen…" + "value": "Zu %@ einladen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Invitar personas…" + "value": "Invitar a %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Inviter des personnes…" + "value": "Inviter dans %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "メンバーを招待…" + "value": "%@ に招待" } }, "ko": { "stringUnit": { "state": "translated", - "value": "구성원 초대…" + "value": "%@에 초대" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "邀请成员…" + "value": "邀请加入 %@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "邀請成員…" + "value": "邀請加入 %@" } } } }, - "sidebar.account.members": { + "cloudInvite.notAdmin.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Members…" + "value": "Only team admins can invite." } }, "ar": { "stringUnit": { "state": "translated", - "value": "الأعضاء…" + "value": "يمكن لمسؤولي الفريق فقط الدعوة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Mitglieder…" + "value": "Nur Team-Admins können einladen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Miembros…" + "value": "Solo los administradores pueden invitar." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Membres…" + "value": "Seuls les administrateurs peuvent inviter." } }, "ja": { "stringUnit": { "state": "translated", - "value": "メンバー…" + "value": "招待できるのはチームの管理者だけです。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "구성원…" + "value": "팀 관리자만 초대할 수 있습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "成员…" + "value": "只有团队管理员可以邀请。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "成員…" + "value": "只有團隊管理員可以邀請。" } } } }, - "command.auth.teamMembers.title": { + "cloudInvite.oneLiner": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite Team Members" + "value": "Members share this team's Cloud machines. Your Mac stays private." } }, "ar": { "stringUnit": { "state": "translated", - "value": "دعوة أعضاء الفريق" + "value": "يتشارك الأعضاء أجهزة Cloud الخاصة بهذا الفريق. يبقى جهاز Mac الخاص بك خاصًا." } }, "de": { "stringUnit": { "state": "translated", - "value": "Teammitglieder einladen" + "value": "Mitglieder teilen die Cloud-Maschinen dieses Teams. Dein Mac bleibt privat." } }, "es": { "stringUnit": { "state": "translated", - "value": "Invitar miembros del equipo" + "value": "Los miembros comparten las máquinas Cloud de este equipo. Tu Mac sigue siendo privado." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Inviter des membres de l’équipe" + "value": "Les membres partagent les machines Cloud de cette équipe. Votre Mac reste privé." } }, "ja": { "stringUnit": { "state": "translated", - "value": "チームメンバーを招待" + "value": "メンバーはこのチームの Cloud マシンを共有します。あなたの Mac は非公開のままです。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "팀 구성원 초대" + "value": "구성원은 이 팀의 Cloud 머신을 공유합니다. 내 Mac은 비공개로 유지됩니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "邀请团队成员" + "value": "成员共享此团队的 Cloud 机器。你的 Mac 保持私密。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "邀請團隊成員" + "value": "成員共用此團隊的 Cloud 機器。你的 Mac 保持私密。" } } } }, - "socket.authTeam.missingInvitation": { + "cloudInvite.seatsFull.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "An invitation id or link id is required." + "value": "No seats left on this plan. Upgrade to Team to invite more." } }, "ar": { "stringUnit": { "state": "translated", - "value": "مطلوب معرّف دعوة أو معرّف رابط." + "value": "لا مقاعد متبقية في هذه الخطة. قم بالترقية إلى Team لدعوة المزيد." } }, "de": { "stringUnit": { "state": "translated", - "value": "Eine Einladungs-ID oder Link-ID ist erforderlich." + "value": "Keine Plätze mehr in diesem Plan. Wechsle zu Team, um weitere einzuladen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Se requiere un ID de invitación o de enlace." + "value": "No quedan asientos en este plan. Cambia a Team para invitar a más." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Un identifiant d’invitation ou de lien est requis." + "value": "Plus de sièges sur cette offre. Passez à Team pour inviter plus de personnes." } }, "ja": { "stringUnit": { "state": "translated", - "value": "招待 ID またはリンク ID が必要です。" + "value": "このプランに空席はありません。Team にアップグレードしてさらに招待してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "초대 ID 또는 링크 ID가 필요합니다." + "value": "이 플랜에 남은 좌석이 없습니다. Team으로 업그레이드하여 더 초대하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需要邀请 ID 或链接 ID。" + "value": "此方案已无席位。请升级到 Team 以邀请更多人。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需要邀請 ID 或連結 ID。" + "value": "此方案已無席位。請升級至 Team 以邀請更多人。" } } } }, - "socket.authTeam.missingUser": { + "cloudInvite.sentTo": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "A user id is required." + "value": "Sent to %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "مطلوب معرّف مستخدم." + "value": "تم الإرسال إلى %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Eine Benutzer-ID ist erforderlich." + "value": "Gesendet an %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Se requiere un ID de usuario." + "value": "Enviado a %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Un identifiant d’utilisateur est requis." + "value": "Envoyé à %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "ユーザー ID が必要です。" + "value": "%@ に送信しました" } }, "ko": { "stringUnit": { "state": "translated", - "value": "사용자 ID가 필요합니다." + "value": "%@에게 보냈습니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需要用户 ID。" + "value": "已发送给 %@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需要使用者 ID。" + "value": "已傳送給 %@" } } } }, - "sidebar.account.invitePeople.short": { + "cli.auth.team.acceptUsage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite People" + "value": "Usage: cmux auth team accept|decline " } }, "ar": { "stringUnit": { "state": "translated", - "value": "دعوة أشخاص" + "value": "الاستخدام: cmux auth team accept|decline " } }, "de": { "stringUnit": { "state": "translated", - "value": "Personen einladen" + "value": "Verwendung: cmux auth team accept|decline " } }, "es": { "stringUnit": { "state": "translated", - "value": "Invitar personas" + "value": "Uso: cmux auth team accept|decline " } }, "fr": { "stringUnit": { "state": "translated", - "value": "Inviter des personnes" + "value": "Utilisation : cmux auth team accept|decline " } }, "ja": { "stringUnit": { "state": "translated", - "value": "メンバーを招待" + "value": "使い方: cmux auth team accept|decline " } }, "ko": { "stringUnit": { "state": "translated", - "value": "구성원 초대" + "value": "사용법: cmux auth team accept|decline " } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "邀请成员" + "value": "用法:cmux auth team accept|decline " } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "邀請成員" + "value": "用法:cmux auth team accept|decline " } } } }, - "sidebar.account.invite.button": { + "cli.auth.team.declined": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite" + "value": "Declined." } }, "ar": { "stringUnit": { "state": "translated", - "value": "دعوة" + "value": "تم الرفض." } }, "de": { "stringUnit": { "state": "translated", - "value": "Einladen" + "value": "Abgelehnt." } }, "es": { "stringUnit": { "state": "translated", - "value": "Invitar" + "value": "Rechazada." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Inviter" + "value": "Refusée." } }, "ja": { "stringUnit": { "state": "translated", - "value": "招待" + "value": "辞退しました。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "초대" + "value": "거절했습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "邀请" + "value": "已拒绝。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "邀請" + "value": "已拒絕。" } } } }, - "cloudInvite.done": { + "cli.auth.team.invitationRow": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Done" + "value": "%1$@ %2$@ (%3$@) invited by %4$@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تم" + "value": "%1$@ %2$@ (%3$@) بدعوة من %4$@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Fertig" + "value": "%1$@ %2$@ (%3$@), eingeladen von %4$@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Listo" + "value": "%1$@ %2$@ (%3$@) invitado por %4$@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Terminé" + "value": "%1$@ %2$@ (%3$@) invité par %4$@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "完了" + "value": "%1$@ %2$@(%3$@)%4$@ からの招待" } }, "ko": { "stringUnit": { "state": "translated", - "value": "완료" + "value": "%1$@ %2$@ (%3$@) %4$@ 님이 초대함" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "完成" + "value": "%1$@ %2$@(%3$@)由 %4$@ 邀请" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "完成" + "value": "%1$@ %2$@(%3$@)由 %4$@ 邀請" } } } }, - "cloudInvite.inviteMore": { + "cli.auth.team.joined": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite More" + "value": "Joined." } }, "ar": { "stringUnit": { "state": "translated", - "value": "دعوة المزيد" + "value": "تم الانضمام." } }, "de": { "stringUnit": { "state": "translated", - "value": "Weitere einladen" + "value": "Beigetreten." } }, "es": { "stringUnit": { "state": "translated", - "value": "Invitar a más" + "value": "Te has unido." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Inviter d’autres personnes" + "value": "Rejoint." } }, "ja": { "stringUnit": { "state": "translated", - "value": "さらに招待" + "value": "参加しました。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "더 초대" + "value": "참여했습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "继续邀请" + "value": "已加入。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "繼續邀請" + "value": "已加入。" } } } }, - "cloudInvite.linkCopied": { + "cli.auth.team.noInvitations": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite link copied. Anyone with it joins as a member for 7 days." + "value": "No pending invitations." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تم نسخ رابط الدعوة. يمكن لأي شخص يملكه الانضمام كعضو لمدة 7 أيام." + "value": "لا توجد دعوات معلقة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Einladungslink kopiert. Wer ihn hat, tritt 7 Tage lang als Mitglied bei." + "value": "Keine ausstehenden Einladungen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Enlace de invitación copiado. Cualquiera que lo tenga se une como miembro durante 7 días." + "value": "No hay invitaciones pendientes." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Lien d’invitation copié. Toute personne qui l’a rejoint comme membre pendant 7 jours." + "value": "Aucune invitation en attente." } }, "ja": { "stringUnit": { "state": "translated", - "value": "招待リンクをコピーしました。リンクを持つ人は 7 日間メンバーとして参加できます。" + "value": "保留中の招待はありません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "초대 링크를 복사했습니다. 링크가 있으면 7일 동안 구성원으로 참여할 수 있습니다." + "value": "대기 중인 초대가 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已复制邀请链接。持有链接者可在 7 天内以成员身份加入。" + "value": "没有待处理的邀请。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已複製邀請連結。持有連結者可在 7 天內以成員身分加入。" + "value": "沒有待處理的邀請。" } } } }, - "cloudInvite.manageMembers": { + "notification.teamInvite.body": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Manage members…" + "value": "Join from the team menu in the Cloud panel or Settings › Account." } }, "ar": { "stringUnit": { "state": "translated", - "value": "إدارة الأعضاء…" + "value": "انضم من قائمة الفريق في لوحة Cloud أو من الإعدادات › الحساب." } }, "de": { "stringUnit": { "state": "translated", - "value": "Mitglieder verwalten…" + "value": "Tritt über das Teammenü im Cloud-Bereich oder unter Einstellungen › Account bei." } }, "es": { "stringUnit": { "state": "translated", - "value": "Gestionar miembros…" + "value": "Únete desde el menú de equipo del panel Cloud o en Ajustes › Cuenta." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Gérer les membres…" + "value": "Rejoignez depuis le menu d'équipe du panneau Cloud ou dans Réglages › Compte." } }, "ja": { "stringUnit": { "state": "translated", - "value": "メンバーを管理…" + "value": "Cloud パネルのチームメニュー、または設定 › アカウントから参加できます。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "구성원 관리…" + "value": "Cloud 패널의 팀 메뉴 또는 설정 › 계정에서 참여하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "管理成员…" + "value": "从 Cloud 面板的团队菜单或“设置 › 账户”加入。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "管理成員…" + "value": "從 Cloud 面板的團隊選單或「設定 › 帳號」加入。" } } } }, - "cloudInvite.partial": { + "notification.teamInvite.someone": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Could not invite: %@" + "value": "A teammate" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تعذّرت الدعوة: %@" + "value": "أحد أعضاء الفريق" } }, "de": { "stringUnit": { "state": "translated", - "value": "Konnte nicht einladen: %@" + "value": "Ein Teammitglied" } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo invitar: %@" + "value": "Un compañero de equipo" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Impossible d’inviter : %@" + "value": "Un coéquipier" } }, "ja": { "stringUnit": { "state": "translated", - "value": "招待できませんでした: %@" + "value": "チームメンバー" } }, "ko": { "stringUnit": { "state": "translated", - "value": "초대할 수 없음: %@" + "value": "팀원" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无法邀请:%@" + "value": "一位队友" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法邀請:%@" + "value": "一位隊友" } } } }, - "cloudInvite.role.admin": { + "notification.teamInvite.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Admin" + "value": "cmux Cloud team" } }, "ar": { "stringUnit": { "state": "translated", - "value": "مسؤول" + "value": "فريق cmux Cloud" } }, "de": { "stringUnit": { "state": "translated", - "value": "Administrator" + "value": "cmux Cloud-Team" } }, "es": { "stringUnit": { "state": "translated", - "value": "Administrador" + "value": "Equipo de cmux Cloud" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Administrateur" + "value": "Équipe cmux Cloud" } }, "ja": { "stringUnit": { "state": "translated", - "value": "管理者" + "value": "cmux Cloud チーム" } }, "ko": { "stringUnit": { "state": "translated", - "value": "관리자" + "value": "cmux Cloud 팀" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "管理员" + "value": "cmux Cloud 团队" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "管理員" + "value": "cmux Cloud 團隊" } } } }, - "cloudInvite.role.label": { + "notification.teamInvite.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Join as" + "value": "%1$@ invited you to %2$@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الانضمام بصفة" + "value": "دعاك %1$@ إلى %2$@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Beitreten als" + "value": "%1$@ hat dich zu %2$@ eingeladen" } }, "es": { "stringUnit": { "state": "translated", - "value": "Unirse como" + "value": "%1$@ te invitó a %2$@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Rejoindre en tant que" + "value": "%1$@ vous a invité à %2$@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "参加する役割" + "value": "%1$@ が %2$@ に招待しました" } }, "ko": { "stringUnit": { "state": "translated", - "value": "참여 역할" + "value": "%1$@ 님이 %2$@에 초대했습니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "加入身份" + "value": "%1$@ 邀请你加入 %2$@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "加入身分" + "value": "%1$@ 邀請你加入 %2$@" } } } }, - "cloudInvite.role.member": { + "cloud.teamPicker.invitedTo": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Member" + "value": "Invited to" } }, "ar": { "stringUnit": { "state": "translated", - "value": "عضو" + "value": "مدعو إلى" } }, "de": { "stringUnit": { "state": "translated", - "value": "Mitglied" + "value": "Eingeladen zu" } }, "es": { "stringUnit": { "state": "translated", - "value": "Miembro" + "value": "Invitado a" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Membre" + "value": "Invité à" } }, "ja": { "stringUnit": { "state": "translated", - "value": "メンバー" + "value": "招待されたチーム" } }, "ko": { "stringUnit": { "state": "translated", - "value": "구성원" + "value": "초대받은 팀" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "成员" + "value": "受邀加入" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "成員" + "value": "受邀加入" } } } }, - "cloudInvite.send": { + "cloud.teamPicker.join": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Send Invite" + "value": "Join %@" } }, "ar": { "stringUnit": { "state": "translated", - "value": "إرسال الدعوة" + "value": "الانضمام إلى %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Einladung senden" + "value": "%@ beitreten" } }, "es": { "stringUnit": { "state": "translated", - "value": "Enviar invitación" + "value": "Unirse a %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Envoyer l’invitation" + "value": "Rejoindre %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "招待を送信" + "value": "%@ に参加" } }, "ko": { "stringUnit": { "state": "translated", - "value": "초대 보내기" + "value": "%@ 참여" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "发送邀请" + "value": "加入 %@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "傳送邀請" + "value": "加入 %@" } } } }, - "cloudInvite.someSent": { + "agentHibernation.manual.agentBusy": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The other invitations were sent." + "value": "This agent is working or waiting for input." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "تم إرسال الدعوات الأخرى." + "value": "このエージェントは作業中か、入力を待っています。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Die übrigen Einladungen wurden gesendet." + "value": "Dieser Agent arbeitet oder wartet auf eine Eingabe." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Las demás invitaciones se enviaron." + "value": "Cet agent travaille ou attend une saisie." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Les autres invitations ont été envoyées." + "value": "هذا الوكيل يعمل أو ينتظر إدخالًا." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "他の招待は送信されました。" + "value": "Este agente está trabajando o esperando una respuesta." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "나머지 초대는 보냈습니다." + "value": "此代理程式正在工作或等待輸入。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "其余邀请已发送。" + "value": "此智能体正在工作或等待输入。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "其餘邀請已送出。" + "value": "이 에이전트는 작업 중이거나 입력을 기다리고 있습니다." } } } }, - "cloudInvite.seatsOpen": { + "agentHibernation.manual.alreadyHibernated": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$d of %#@seats@ open on this plan." - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%d seat" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%d seats" - } - } - } - } - } + "value": "This agent is already hibernated." } }, - "de": { + "ja": { "stringUnit": { "state": "translated", - "value": "%1$d von %#@seats@ dieses Plans frei." - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%d Platz" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%d Plätzen" - } - } - } - } - } + "value": "このエージェントはすでに休止中です。" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "%1$d sur %#@seats@ libres sur cette offre." - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%d siège" - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "%d sièges" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%d sièges" - } - } - } - } - } + "value": "Dieser Agent ruht bereits." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$d de %#@seats@ libres en este plan." - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%d asiento" - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "%d asientos" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%d asientos" - } - } - } - } - } + "value": "Cet agent est déjà en veille." } }, "ar": { "stringUnit": { "state": "translated", - "value": "%1$d من %#@seats@ متاحة في هذه الخطة." - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "zero": { - "stringUnit": { - "state": "translated", - "value": "%d مقعد" - } - }, - "one": { - "stringUnit": { - "state": "translated", - "value": "مقعد واحد (%d)" - } - }, - "two": { - "stringUnit": { - "state": "translated", - "value": "مقعدين (%d)" - } - }, - "few": { - "stringUnit": { - "state": "translated", - "value": "%d مقاعد" - } - }, - "many": { - "stringUnit": { - "state": "translated", - "value": "%d مقعدًا" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%d مقعد" - } - } - } - } - } + "value": "هذا الوكيل في وضع السبات بالفعل." } }, - "zh-Hant": { + "es": { "stringUnit": { "state": "translated", - "value": "此方案尚有 %1$d / %#@seats@ 可用。" - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%d 個席位" - } - } - } - } - } + "value": "Este agente ya está en hibernación." } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此方案尚有 %1$d / %#@seats@ 可用。" - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%d 个席位" - } - } - } - } - } + "value": "此代理程式已經休眠。" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "이 플랜에서 %1$d / %#@seats@ 남음." - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%d석" - } - } - } - } - } + "value": "此智能体已休眠。" } }, - "ja": { + "ko": { "stringUnit": { "state": "translated", - "value": "このプランで %1$d / %#@seats@ が空いています。" - }, - "substitutions": { - "seats": { - "argNum": 2, - "formatSpecifier": "d", - "variations": { - "plural": { - "other": { - "stringUnit": { - "state": "translated", - "value": "%d 席" - } - } - } - } - } + "value": "이 에이전트는 이미 휴면 중입니다." } } } }, - "cloudInvite.copyLink.short": { + "agentHibernation.manual.lifecycleUnknown": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Copy link" + "value": "cmux can't tell whether this agent is idle, so it was left running." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "نسخ الرابط" + "value": "cmux はこのエージェントがアイドル状態か判断できないため、実行したままにしました。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Link kopieren" + "value": "cmux kann nicht erkennen, ob dieser Agent untätig ist, daher läuft er weiter." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Copiar enlace" + "value": "cmux ne peut pas savoir si cet agent est inactif, il continue donc de tourner." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Copier le lien" + "value": "لا يستطيع cmux معرفة ما إذا كان هذا الوكيل خاملًا، لذا تُرك قيد التشغيل." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "リンクをコピー" + "value": "cmux no puede saber si este agente está inactivo, así que sigue en ejecución." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "링크 복사" + "value": "cmux 無法判斷此代理程式是否閒置,因此保持執行。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "复制链接" + "value": "cmux 无法判断此智能体是否空闲,因此保持运行。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "複製連結" + "value": "cmux가 이 에이전트의 유휴 여부를 알 수 없어 실행 상태로 두었습니다." } } } }, - "cloudInvite.emails.placeholder.short": { + "agentHibernation.manual.notRestorable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Email, comma separated" + "value": "This terminal has no agent that cmux can resume the way it was started." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "البريد الإلكتروني، مفصول بفواصل" + "value": "このターミナルには、起動時と同じ方法で cmux が再開できるエージェントがありません。" } }, "de": { "stringUnit": { "state": "translated", - "value": "E-Mail, durch Kommas getrennt" + "value": "Dieses Terminal hat keinen Agenten, den cmux so fortsetzen kann, wie er gestartet wurde." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Correo, separado por comas" + "value": "Ce terminal n’a aucun agent que cmux peut reprendre de la façon dont il a été lancé." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "E-mail, séparés par des virgules" + "value": "لا تحتوي هذه الطرفية على وكيل يستطيع cmux استئنافه بالطريقة التي بدأ بها." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "メール(カンマ区切り)" + "value": "Esta terminal no tiene un agente que cmux pueda reanudar tal como se inició." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이메일(쉼표로 구분)" + "value": "此終端機沒有 cmux 能以原本啟動方式恢復的代理程式。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "电子邮件,以逗号分隔" + "value": "此终端没有 cmux 能按原启动方式恢复的智能体。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "電子郵件,以逗號分隔" + "value": "이 터미널에는 시작된 방식 그대로 cmux가 재개할 수 있는 에이전트가 없습니다." } } } }, - "cloudInvite.heading.short": { + "agentHibernation.manual.notRunning": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invite to %@" + "value": "This terminal is not running, so there is nothing to hibernate." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "دعوة إلى %@" + "value": "このターミナルは実行されていないため、休止するものがありません。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Zu %@ einladen" + "value": "Dieses Terminal läuft nicht, daher gibt es nichts zum Ruhen." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Invitar a %@" + "value": "Ce terminal n’est pas en cours d’exécution, il n’y a donc rien à mettre en veille." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Inviter dans %@" + "value": "هذه الطرفية لا تعمل، لذا لا يوجد ما يمكن وضعه في السبات." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "%@ に招待" + "value": "Esta terminal no se está ejecutando, así que no hay nada que hibernar." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%@에 초대" + "value": "此終端機未在執行,沒有可休眠的內容。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "邀请加入 %@" + "value": "此终端未在运行,没有可休眠的内容。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "邀請加入 %@" + "value": "이 터미널은 실행 중이 아니어서 휴면할 대상이 없습니다." } } } }, - "cloudInvite.notAdmin.short": { + "agentHibernation.manual.processScopeUnsafe": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Only team admins can invite." + "value": "This terminal is running other work, such as background jobs, that hibernation would stop." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "يمكن لمسؤولي الفريق فقط الدعوة." + "value": "このターミナルでは、休止すると停止してしまうバックグラウンドジョブなどの作業が実行中です。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Nur Team-Admins können einladen." + "value": "In diesem Terminal läuft weitere Arbeit, etwa Hintergrundjobs, die das Ruhen beenden würde." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Solo los administradores pueden invitar." + "value": "Ce terminal exécute d’autres tâches, comme des tâches en arrière-plan, que la mise en veille arrêterait." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Seuls les administrateurs peuvent inviter." + "value": "تشغّل هذه الطرفية أعمالًا أخرى، مثل مهام الخلفية، سيوقفها السبات." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "招待できるのはチームの管理者だけです。" + "value": "Esta terminal está ejecutando otro trabajo, como tareas en segundo plano, que la hibernación detendría." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "팀 관리자만 초대할 수 있습니다." + "value": "此終端機正在執行其他工作(例如背景工作),休眠會使其停止。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "只有团队管理员可以邀请。" + "value": "此终端正在运行其他工作(例如后台任务),休眠会使其停止。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "只有團隊管理員可以邀請。" + "value": "이 터미널에서 백그라운드 작업 등 휴면하면 중단될 다른 작업이 실행 중입니다." } } } }, - "cloudInvite.oneLiner": { + "agentHibernation.manual.recentlyActive": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Members share this team's Cloud machines. Your Mac stays private." + "value": "This agent was active a moment ago. Try again in a few seconds." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "يتشارك الأعضاء أجهزة Cloud الخاصة بهذا الفريق. يبقى جهاز Mac الخاص بك خاصًا." + "value": "このエージェントは少し前まで動作していました。数秒後にもう一度お試しください。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Mitglieder teilen die Cloud-Maschinen dieses Teams. Dein Mac bleibt privat." + "value": "Dieser Agent war gerade eben aktiv. Versuche es in ein paar Sekunden erneut." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Los miembros comparten las máquinas Cloud de este equipo. Tu Mac sigue siendo privado." + "value": "Cet agent était actif il y a un instant. Réessayez dans quelques secondes." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Les membres partagent les machines Cloud de cette équipe. Votre Mac reste privé." + "value": "كان هذا الوكيل نشطًا قبل لحظة. حاول مرة أخرى بعد بضع ثوانٍ." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "メンバーはこのチームの Cloud マシンを共有します。あなたの Mac は非公開のままです。" + "value": "Este agente estuvo activo hace un momento. Inténtalo de nuevo en unos segundos." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "구성원은 이 팀의 Cloud 머신을 공유합니다. 내 Mac은 비공개로 유지됩니다." + "value": "此代理程式剛剛仍在活動。請幾秒後再試。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "成员共享此团队的 Cloud 机器。你的 Mac 保持私密。" + "value": "此智能体刚刚仍在活动。请几秒后再试。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "成員共用此團隊的 Cloud 機器。你的 Mac 保持私密。" + "value": "이 에이전트는 방금 전까지 활동했습니다. 몇 초 후 다시 시도하세요." } } } }, - "cloudInvite.seatsFull.short": { + "agentHibernation.manual.surfaceNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No seats left on this plan. Upgrade to Team to invite more." + "value": "No terminal matches that surface." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "لا مقاعد متبقية في هذه الخطة. قم بالترقية إلى Team لدعوة المزيد." + "value": "そのサーフェスに一致するターミナルはありません。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Keine Plätze mehr in diesem Plan. Wechsle zu Team, um weitere einzuladen." + "value": "Kein Terminal passt zu dieser Oberfläche." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "No quedan asientos en este plan. Cambia a Team para invitar a más." + "value": "Aucun terminal ne correspond à cette surface." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Plus de sièges sur cette offre. Passez à Team pour inviter plus de personnes." + "value": "لا توجد طرفية تطابق هذا السطح." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "このプランに空席はありません。Team にアップグレードしてさらに招待してください。" + "value": "Ninguna terminal coincide con esa superficie." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "이 플랜에 남은 좌석이 없습니다. Team으로 업그레이드하여 더 초대하세요." + "value": "沒有符合該 surface 的終端機。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此方案已无席位。请升级到 Team 以邀请更多人。" + "value": "没有与该 surface 匹配的终端。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "此方案已無席位。請升級至 Team 以邀請更多人。" + "value": "해당 서피스와 일치하는 터미널이 없습니다." } } } }, - "cloudInvite.sentTo": { + "agentHibernation.manual.teardownInProgress": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sent to %@" + "value": "This agent is already being hibernated." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "تم الإرسال إلى %@" + "value": "このエージェントはすでに休止処理中です。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Gesendet an %@" + "value": "Dieser Agent wird bereits in den Ruhezustand versetzt." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Enviado a %@" + "value": "Cet agent est déjà en cours de mise en veille." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Envoyé à %@" + "value": "يجري بالفعل وضع هذا الوكيل في السبات." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "%@ に送信しました" + "value": "Este agente ya se está hibernando." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%@에게 보냈습니다" + "value": "此代理程式正在休眠中。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已发送给 %@" + "value": "此智能体正在休眠中。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "已傳送給 %@" + "value": "이 에이전트는 이미 휴면 처리 중입니다." } } } }, - "cli.auth.team.acceptUsage": { + "agentHibernation.manual.teardownRefused": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux auth team accept|decline " + "value": "The agent changed or its session could not be protected, so it was left running. Try again later." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux auth team accept|decline " + "value": "エージェントが変化したか、セッションを保護できなかったため、実行したままにしました。後でもう一度お試しください。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux auth team accept|decline " + "value": "Der Agent hat sich geändert oder seine Sitzung konnte nicht geschützt werden, daher läuft er weiter. Versuche es später erneut." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Uso: cmux auth team accept|decline " + "value": "L’agent a changé ou sa session n’a pas pu être protégée, il continue donc de tourner. Réessayez plus tard." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux auth team accept|decline " + "value": "تغيّر الوكيل أو تعذّرت حماية جلسته، لذا تُرك قيد التشغيل. حاول مرة أخرى لاحقًا." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "使い方: cmux auth team accept|decline " + "value": "El agente cambió o no se pudo proteger su sesión, así que sigue en ejecución. Inténtalo más tarde." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "사용법: cmux auth team accept|decline " + "value": "代理程式有變動或其工作階段無法受保護,因此保持執行。請稍後再試。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team accept|decline " + "value": "智能体有变化或其会话无法受到保护,因此保持运行。请稍后再试。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "用法:cmux auth team accept|decline " + "value": "에이전트가 변경되었거나 세션을 보호할 수 없어 실행 상태로 두었습니다. 나중에 다시 시도하세요." } } } }, - "cli.auth.team.declined": { + "agentHibernation.manual.unconfirmedInput": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Declined." + "value": "This agent has typed input it has not received yet." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "تم الرفض." + "value": "このエージェントには、まだ受け取っていない入力があります。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Abgelehnt." + "value": "Dieser Agent hat getippte Eingaben, die er noch nicht erhalten hat." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Rechazada." + "value": "Cet agent a une saisie qu’il n’a pas encore reçue." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Refusée." + "value": "لدى هذا الوكيل إدخال مكتوب لم يستلمه بعد." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "辞退しました。" + "value": "Este agente tiene texto escrito que aún no ha recibido." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "거절했습니다." + "value": "此代理程式有尚未收到的輸入。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已拒绝。" + "value": "此智能体有尚未收到的输入。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "已拒絕。" + "value": "이 에이전트에는 아직 받지 않은 입력이 있습니다." } } } }, - "cli.auth.team.invitationRow": { + "agentHibernation.manual.visible": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@ (%3$@) invited by %4$@" + "value": "This agent is on screen. Switch away from it first." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@ (%3$@) بدعوة من %4$@" + "value": "このエージェントは画面に表示されています。先に別の場所に切り替えてください。" } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@ (%3$@), eingeladen von %4$@" + "value": "Dieser Agent ist auf dem Bildschirm. Wechsle zuerst weg." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@ (%3$@) invitado por %4$@" + "value": "Cet agent est à l’écran. Passez d’abord à autre chose." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@ (%3$@) invité par %4$@" + "value": "هذا الوكيل ظاهر على الشاشة. انتقل بعيدًا عنه أولًا." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@(%3$@)%4$@ からの招待" + "value": "Este agente está en pantalla. Cambia a otro lugar primero." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@ (%3$@) %4$@ 님이 초대함" + "value": "此代理程式正顯示在螢幕上。請先切換離開。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@(%3$@)由 %4$@ 邀请" + "value": "此智能体正显示在屏幕上。请先切换离开。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "%1$@ %2$@(%3$@)由 %4$@ 邀請" + "value": "이 에이전트가 화면에 표시되어 있습니다. 먼저 다른 곳으로 전환하세요." } } } }, - "cli.auth.team.joined": { + "agentHibernation.wake.notHibernated": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Joined." + "value": "This agent is not hibernated." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "تم الانضمام." + "value": "このエージェントは休止していません。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Beigetreten." + "value": "Dieser Agent ruht nicht." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Te has unido." + "value": "Cet agent n’est pas en veille." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Rejoint." + "value": "هذا الوكيل ليس في وضع السبات." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "参加しました。" + "value": "Este agente no está en hibernación." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "참여했습니다." + "value": "此代理程式未休眠。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已加入。" + "value": "此智能体未休眠。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "已加入。" + "value": "이 에이전트는 휴면 중이 아닙니다." } } } }, - "cli.auth.team.noInvitations": { + "agentHibernation.wake.resumeUnavailable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No pending invitations." + "value": "cmux could not start this agent's resume command." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "لا توجد دعوات معلقة." + "value": "cmux はこのエージェントの再開コマンドを開始できませんでした。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Keine ausstehenden Einladungen." + "value": "cmux konnte den Fortsetzungsbefehl dieses Agenten nicht starten." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "No hay invitaciones pendientes." + "value": "cmux n’a pas pu lancer la commande de reprise de cet agent." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Aucune invitation en attente." + "value": "تعذّر على cmux بدء أمر الاستئناف لهذا الوكيل." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "保留中の招待はありません。" + "value": "cmux no pudo iniciar el comando de reanudación de este agente." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "대기 중인 초대가 없습니다." + "value": "cmux 無法啟動此代理程式的恢復指令。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "没有待处理的邀请。" + "value": "cmux 无法启动此智能体的恢复命令。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "沒有待處理的邀請。" + "value": "cmux가 이 에이전트의 재개 명령을 시작할 수 없습니다." } } } }, - "notification.teamInvite.body": { + "agentHibernation.wake.surfaceNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Join from the team menu in the Cloud panel or Settings › Account." + "value": "No terminal matches that surface." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "انضم من قائمة الفريق في لوحة Cloud أو من الإعدادات › الحساب." + "value": "そのサーフェスに一致するターミナルはありません。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Tritt über das Teammenü im Cloud-Bereich oder unter Einstellungen › Account bei." + "value": "Kein Terminal passt zu dieser Oberfläche." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Únete desde el menú de equipo del panel Cloud o en Ajustes › Cuenta." + "value": "Aucun terminal ne correspond à cette surface." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Rejoignez depuis le menu d'équipe du panneau Cloud ou dans Réglages › Compte." + "value": "لا توجد طرفية تطابق هذا السطح." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "Cloud パネルのチームメニュー、または設定 › アカウントから参加できます。" + "value": "Ninguna terminal coincide con esa superficie." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud 패널의 팀 메뉴 또는 설정 › 계정에서 참여하세요." + "value": "沒有符合該 surface 的終端機。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "从 Cloud 面板的团队菜单或“设置 › 账户”加入。" + "value": "没有与该 surface 匹配的终端。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "從 Cloud 面板的團隊選單或「設定 › 帳號」加入。" + "value": "해당 서피스와 일치하는 터미널이 없습니다." } } } }, - "notification.teamInvite.someone": { + "agentHibernation.wake.teardownInProgress": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "A teammate" + "value": "This agent is still being hibernated. Try again in a moment." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "أحد أعضاء الفريق" + "value": "このエージェントはまだ休止処理中です。しばらくしてからもう一度お試しください。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ein Teammitglied" + "value": "Dieser Agent wird noch in den Ruhezustand versetzt. Versuche es gleich noch einmal." } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Un compañero de equipo" + "value": "Cet agent est encore en cours de mise en veille. Réessayez dans un instant." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Un coéquipier" + "value": "لا يزال هذا الوكيل قيد الوضع في السبات. حاول مرة أخرى بعد لحظة." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "チームメンバー" + "value": "Este agente todavía se está hibernando. Inténtalo en un momento." } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "팀원" + "value": "此代理程式仍在休眠中。請稍後再試。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "一位队友" + "value": "此智能体仍在休眠中。请稍后再试。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "一位隊友" + "value": "이 에이전트는 아직 휴면 처리 중입니다. 잠시 후 다시 시도하세요." } } } }, - "notification.teamInvite.subtitle": { + "cli.agentHibernation.hibernated": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux Cloud team" + "value": "OK hibernated %@" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "فريق cmux Cloud" + "value": "OK 休止しました %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux Cloud-Team" + "value": "OK ruht jetzt %@" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Equipo de cmux Cloud" + "value": "OK mis en veille %@" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Équipe cmux Cloud" + "value": "OK تم وضعه في السبات %@" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux Cloud チーム" + "value": "OK hibernado %@" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux Cloud 팀" + "value": "OK 已休眠 %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux Cloud 团队" + "value": "OK 已休眠 %@" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux Cloud 團隊" + "value": "OK 휴면함 %@" } } } }, - "notification.teamInvite.title": { + "cli.agentHibernation.usage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@ invited you to %2$@" + "value": "Usage: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "دعاك %1$@ إلى %2$@" + "value": "使い方: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@ hat dich zu %2$@ eingeladen" + "value": "Verwendung: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$@ te invitó a %2$@" + "value": "Utilisation: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "%1$@ vous a invité à %2$@" + "value": "الاستخدام: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "%1$@ が %2$@ に招待しました" + "value": "Uso: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@ 님이 %2$@에 초대했습니다" + "value": "用法: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@ 邀请你加入 %2$@" + "value": "用法: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "%1$@ 邀請你加入 %2$@" + "value": "사용법: cmux agent-hibernation [--json]\n cmux agent-hibernation [--workspace ] [--json]" } } } }, - "cloud.teamPicker.invitedTo": { + "cli.agentHibernation.woke": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Invited to" + "value": "OK woke %@" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "مدعو إلى" + "value": "OK 再開しました %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Eingeladen zu" + "value": "OK geweckt %@" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Invitado a" + "value": "OK réveillé %@" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Invité à" + "value": "OK تم إيقاظه %@" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "招待されたチーム" + "value": "OK reanudado %@" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "초대받은 팀" + "value": "OK 已喚醒 %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "受邀加入" + "value": "OK 已唤醒 %@" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "受邀加入" + "value": "OK 깨움 %@" } } } }, - "cloud.teamPicker.join": { + "socket.agentHibernation.surfaceRequired": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Join %@" + "value": "Missing or invalid surface_id" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "الانضمام إلى %@" + "value": "surface_id がないか無効です" } }, "de": { "stringUnit": { "state": "translated", - "value": "%@ beitreten" + "value": "surface_id fehlt oder ist ungültig" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "Unirse a %@" + "value": "surface_id manquant ou invalide" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Rejoindre %@" + "value": "surface_id مفقود أو غير صالح" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "%@ に参加" + "value": "Falta surface_id o no es válido" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%@ 참여" + "value": "缺少 surface_id 或其無效" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "加入 %@" + "value": "缺少 surface_id 或其无效" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "加入 %@" + "value": "surface_id가 없거나 올바르지 않습니다" } } } @@ -601716,6 +601712,954 @@ } } } + }, + "cli.pr.error.ambiguousWorkspace": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: could not identify the caller workspace; run it inside a cmux terminal or pass --workspace " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: Der aufrufende Workspace konnte nicht ermittelt werden. Führen Sie den Befehl in einem cmux-Terminal aus oder übergeben Sie --workspace ." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux pr : impossible d’identifier le workspace appelant ; exécutez la commande dans un terminal cmux ou indiquez --workspace " + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: تعذر تحديد مساحة العمل المستدعية؛ شغّله داخل طرفية cmux أو مرّر --workspace " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: no se pudo identificar el espacio de trabajo que llama; ejecútalo en un terminal de cmux o pasa --workspace " + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux pr:無法識別呼叫端工作區;請在 cmux 終端機中執行,或傳入 --workspace " + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux pr:无法识别调用方工作区;请在 cmux 终端中运行,或传入 --workspace " + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: 호출한 워크스페이스를 식별할 수 없습니다. cmux 터미널 안에서 실행하거나 --workspace 를 전달하세요" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: 呼び出し元ワークスペースを特定できません。cmux ターミナル内で実行するか --workspace を渡してください" + } + } + } + }, + "cli.pr.error.notRepository": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux pr requires a git repository in the current directory" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux pr benötigt ein Git-Repository im aktuellen Verzeichnis" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux pr nécessite un dépôt Git dans le répertoire courant" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يتطلب cmux pr مستودع Git في الدليل الحالي" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux pr requiere un repositorio Git en el directorio actual" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 需要目前目錄中的 Git 儲存庫" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 需要当前目录中的 Git 仓库" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux pr은 현재 디렉터리에 Git 저장소가 필요합니다" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux pr には現在のディレクトリに Git リポジトリが必要です" + } + } + } + }, + "cli.pr.error.invalidSelector": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux pr expects a pull-request URL or a positive pull-request number" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux pr erwartet eine Pull-Request-URL oder eine positive Pull-Request-Nummer" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux pr attend une URL de pull request ou un numéro positif" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يتوقع cmux pr رابط طلب سحب أو رقمًا موجبًا" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux pr espera una URL de pull request o un número positivo" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 需要拉取請求網址或正整數編號" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 需要拉取请求网址或正整数编号" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux pr은 풀 리퀘스트 URL 또는 양의 풀 리퀘스트 번호를 요구합니다" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux pr にはプルリクエスト URL または正の番号が必要です" + } + } + } + }, + "cli.pr.error.lookupFailed": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux pr could not resolve the pull request; check authentication and try again" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux pr konnte den Pull Request nicht auflösen; prüfen Sie die Anmeldung und versuchen Sie es erneut" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux pr n’a pas pu résoudre la pull request ; vérifiez l’authentification puis réessayez" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذر على cmux pr العثور على طلب السحب؛ تحقق من تسجيل الدخول ثم حاول مرة أخرى" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux pr no pudo resolver la pull request; comprueba la autenticación y vuelve a intentarlo" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 無法解析拉取請求;請檢查登入狀態後再試" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 无法解析拉取请求;请检查登录状态后重试" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux pr에서 풀 리퀘스트를 확인할 수 없습니다. 인증 상태를 확인한 후 다시 시도하세요" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux pr はプルリクエストを解決できませんでした。認証を確認して再試行してください" + } + } + } + }, + "cli.pr.error.lookupMalformed": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux pr received invalid pull-request information" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux pr erhielt ungültige Pull-Request-Informationen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux pr a reçu des informations de pull request invalides" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تلقى cmux pr معلومات غير صالحة عن طلب السحب" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux pr recibió información no válida de la pull request" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 收到無效的拉取請求資訊" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux pr 收到无效的拉取请求信息" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux pr에서 잘못된 풀 리퀘스트 정보를 받았습니다" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux pr は無効なプルリクエスト情報を受け取りました" + } + } + } + }, + "cli.pr.error.repositoryMismatch": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: the pull request does not belong to the detected repository" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: Der Pull Request gehört nicht zum erkannten Repository" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cmux pr : la pull request n’appartient pas au dépôt détecté" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: لا ينتمي طلب السحب إلى المستودع المكتشف" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: la pull request no pertenece al repositorio detectado" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux pr:拉取請求不屬於偵測到的儲存庫" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux pr:拉取请求不属于检测到的仓库" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: 풀 리퀘스트가 감지된 저장소에 속하지 않습니다" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux pr: プルリクエストは検出されたリポジトリに属していません" + } + } + } + }, + "cli.pr.usage": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Usage: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nAttach or replace a pull-request link immediately. Requires a Git repository and account authentication.\nUses the repository associated with the current directory, including its configured upstream.\nTarget: explicit workspace, caller TTY, configured workspace, then a unique worktree match.\n--window restricts resolution; ambiguous targets fail without changing focus.\nThe manual link survives branch refreshes until replaced, cleared, or the session ends.\nThe existing watcher refreshes matching pull-request status. Clear removes only the manual link.\nSidebar visibility and click settings still apply.\n\nExample:\n cmux pr 123\n cmux pr clear" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verwendung: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nVerknüpft oder ersetzt sofort einen Pull-Request-Link. Erfordert ein Git-Repository und eine authentifizierte Anmeldung.\nVerwendet das Repository des aktuellen Verzeichnisses einschließlich des konfigurierten Upstreams.\nZiel: expliziter Workspace, aufrufendes TTY, konfigurierter Workspace, dann ein eindeutiger Worktree-Treffer.\n--window begrenzt die Suche; mehrdeutige Ziele schlagen ohne Fokuswechsel fehl.\nDer manuelle Link bleibt bei Branch-Aktualisierungen bis zum Ersetzen, Löschen oder Sitzungsende erhalten.\nDer vorhandene Watcher aktualisiert den passenden Pull-Request-Status. Clear entfernt nur den manuellen Link.\nSidebar-Sichtbarkeits- und Klickeinstellungen gelten weiterhin.\n\nBeispiel:\n cmux pr 123\n cmux pr clear" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisation : cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nAssocie ou remplace immédiatement un lien de pull request. Nécessite un dépôt Git et une authentification.\nUtilise le dépôt associé au répertoire courant, y compris son amont configuré.\nCible : workspace explicite, TTY appelant, workspace configuré, puis worktree unique correspondant.\n--window restreint la recherche ; une cible ambiguë échoue sans changer le focus.\nLe lien manuel survit aux actualisations de branche jusqu’à son remplacement, sa suppression ou la fin de session.\nLe suivi existant actualise le statut de la pull request correspondante. Clear supprime uniquement le lien manuel.\nLes réglages de visibilité et de clic de la barre latérale restent applicables.\n\nExemple :\n cmux pr 123\n cmux pr clear" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الاستخدام: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nيربط رابط طلب سحب أو يستبدله فورًا. يتطلب مستودع Git وتسجيل دخول موثق.\nيستخدم المستودع المرتبط بالدليل الحالي، بما في ذلك المنبع المضبوط.\nالهدف: مساحة عمل صريحة، ثم TTY المستدعي، ثم مساحة العمل المضبوطة، ثم تطابق worktree فريد.\nيقيّد --window البحث؛ تفشل الأهداف الملتبسة دون تغيير التركيز.\nيبقى الرابط اليدوي بعد تحديثات الفرع حتى استبداله أو مسحه أو انتهاء الجلسة.\nيحدّث المراقب الحالي حالة طلب السحب المطابق. يزيل clear الرابط اليدوي فقط.\nتظل إعدادات ظهور الشريط الجانبي والنقر سارية.\n\nمثال:\n cmux pr 123\n cmux pr clear" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Uso: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nAsocia o reemplaza inmediatamente un enlace de pull request. Requiere un repositorio Git y autenticación.\nUsa el repositorio asociado al directorio actual, incluido su upstream configurado.\nDestino: espacio explícito, TTY del llamante, espacio configurado y después un worktree coincidente único.\n--window limita la búsqueda; los destinos ambiguos fallan sin cambiar el foco.\nEl enlace manual sobrevive a las actualizaciones de rama hasta reemplazarlo, borrarlo o finalizar la sesión.\nEl observador existente actualiza el estado de la pull request coincidente. Clear solo elimina el enlace manual.\nSe siguen aplicando los ajustes de visibilidad y clic de la barra lateral.\n\nEjemplo:\n cmux pr 123\n cmux pr clear" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "用法:cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\n立即附加或取代拉取請求連結。需要 Git 儲存庫與已驗證的登入狀態。\n使用目前目錄所關聯的儲存庫,包括已設定的上游。\n目標順序:明確工作區、呼叫端 TTY、已設定工作區,然後是唯一符合的 worktree。\n--window 會限制搜尋;目標不明確時會失敗且不變更焦點。\n手動連結會保留於分支重新整理後,直到被取代、清除或工作階段結束。\n現有監看器會更新相符拉取請求的狀態。clear 只移除手動連結。\n側邊欄顯示與點擊設定仍然適用。\n\n範例:\n cmux pr 123\n cmux pr clear" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "用法:cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\n立即附加或替换拉取请求链接。需要 Git 仓库和已验证的登录状态。\n使用当前目录关联的仓库,包括已配置的上游。\n目标顺序:明确工作区、调用方 TTY、已配置工作区,然后是唯一匹配的 worktree。\n--window 限制搜索;目标不明确时会失败且不改变焦点。\n手动链接在分支刷新后仍然保留,直到被替换、清除或会话结束。\n现有监视器会更新匹配拉取请求的状态。clear 只移除手动链接。\n侧边栏显示和点击设置仍然适用。\n\n示例:\n cmux pr 123\n cmux pr clear" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사용법: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\n풀 리퀘스트 링크를 즉시 연결하거나 교체합니다. Git 저장소와 인증된 로그인이 필요합니다.\n현재 디렉터리에 연결된 저장소와 설정된 업스트림을 사용합니다.\n대상 순서: 명시적 워크스페이스, 호출 TTY, 설정된 워크스페이스, 고유한 worktree 일치 항목.\n--window는 검색 범위를 제한하며, 모호한 대상은 포커스를 바꾸지 않고 실패합니다.\n수동 링크는 교체, 삭제 또는 세션 종료까지 브랜치 새로고침 후에도 유지됩니다.\n기존 감시기가 일치하는 풀 리퀘스트 상태를 갱신합니다. clear는 수동 링크만 제거합니다.\n사이드바 표시 및 클릭 설정은 계속 적용됩니다.\n\n예:\n cmux pr 123\n cmux pr clear" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "使用方法: cmux pr [--workspace ] [--window ]\n cmux pr clear [--workspace ] [--window ]\n\nプルリクエストのリンクをすぐに追加または置換します。Git リポジトリと認証済みのログインが必要です。\n現在のディレクトリに関連付けられたリポジトリと、設定済みの上流を使用します。\n対象の優先順:明示ワークスペース、呼び出し元 TTY、設定済みワークスペース、一意の worktree 一致。\n--window は検索を制限します。対象が曖昧な場合、フォーカスを変えずに失敗します。\n手動リンクはブランチ更新後も、置換・削除またはセッション終了まで保持されます。\n既存の監視処理が一致するプルリクエストの状態を更新します。clear は手動リンクだけを削除します。\nサイドバーの表示設定とクリック設定は引き続き適用されます。\n\n例:\n cmux pr 123\n cmux pr clear" + } + } + } + }, + "cli.pr.error.workspaceMissing": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Workspace not found; run cmux list-workspaces and retry with --workspace." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Workspace nicht gefunden; führen Sie cmux list-workspaces aus und wiederholen Sie den Befehl mit --workspace." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Workspace introuvable ; lancez cmux list-workspaces puis réessayez avec --workspace." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يتم العثور على مساحة العمل؛ شغّل cmux list-workspaces ثم أعد المحاولة باستخدام --workspace." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se encontró el espacio de trabajo; ejecuta cmux list-workspaces y reintenta con --workspace." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "找不到工作區;請執行 cmux list-workspaces,然後使用 --workspace 重試。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "找不到工作区;请运行 cmux list-workspaces,然后使用 --workspace 重试。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "워크스페이스를 찾을 수 없습니다. cmux list-workspaces를 실행하고 --workspace로 다시 시도하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークスペースが見つかりません。cmux list-workspaces を実行し、--workspace で再試行してください。" + } + } + } + }, + "cli.pr.error.invalidHandoff": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invalid PR handoff; use cmux pr --help." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ungültige PR-Zuordnung; verwenden Sie cmux pr --help." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Association de PR invalide ; consultez cmux pr --help." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ربط PR غير صالح؛ استخدم cmux pr --help." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Asociación de PR no válida; usa cmux pr --help." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "PR 關聯無效;請使用 cmux pr --help。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "PR 关联无效;请使用 cmux pr --help。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "잘못된 PR 연결입니다. cmux pr --help를 사용하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "無効な PR 関連付けです。cmux pr --help を参照してください。" + } + } + } + }, + "cli.help.command.pr": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "pr [flags] | pr clear [flags]" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "pr [Optionen] | pr clear [Optionen]" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pr [options] | pr clear [options]" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "pr [الخيارات] | pr clear [الخيارات]" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "pr [opciones] | pr clear [opciones]" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "pr [旗標] | pr clear [旗標]" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "pr [标志] | pr clear [标志]" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "pr [옵션] | pr clear [옵션]" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "pr [オプション] | pr clear [オプション]" + } + } + } + }, + "update.readyWaiting.agents": { + "extractionState": "manual", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Installs and relaunches when %lld agent finishes. Agents resume after the relaunch." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Installs and relaunches when %lld agents finish. Agents resume after the relaunch." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Wird installiert und neu gestartet, sobald %lld Agent fertig ist. Agenten werden nach dem Neustart fortgesetzt." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Wird installiert und neu gestartet, sobald %lld Agenten fertig sind. Agenten werden nach dem Neustart fortgesetzt." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "S’installe et redémarre quand %lld agent a terminé. Les agents reprennent après le redémarrage." + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "S’installe et redémarre quand %lld d’agents ont terminé. Les agents reprennent après le redémarrage." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "S’installe et redémarre quand %lld agents ont terminé. Les agents reprennent après le redémarrage." + } + } + } + } + }, + "ar": { + "variations": { + "plural": { + "zero": { + "stringUnit": { + "state": "translated", + "value": "يتم التثبيت وإعادة التشغيل عند انتهاء الوكلاء (%lld). تستأنف الوكلاء بعد إعادة التشغيل." + } + }, + "one": { + "stringUnit": { + "state": "translated", + "value": "يتم التثبيت وإعادة التشغيل عند انتهاء وكيل واحد (%lld). تستأنف الوكلاء بعد إعادة التشغيل." + } + }, + "two": { + "stringUnit": { + "state": "translated", + "value": "يتم التثبيت وإعادة التشغيل عند انتهاء وكيلين (%lld). تستأنف الوكلاء بعد إعادة التشغيل." + } + }, + "few": { + "stringUnit": { + "state": "translated", + "value": "يتم التثبيت وإعادة التشغيل عند انتهاء %lld وكلاء. تستأنف الوكلاء بعد إعادة التشغيل." + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "يتم التثبيت وإعادة التشغيل عند انتهاء %lld وكيلًا. تستأنف الوكلاء بعد إعادة التشغيل." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "يتم التثبيت وإعادة التشغيل عند انتهاء %lld وكيل. تستأنف الوكلاء بعد إعادة التشغيل." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Se instala y reinicia cuando termine %lld agente. Los agentes se reanudan tras el reinicio." + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "Se instala y reinicia cuando terminen %lld de agentes. Los agentes se reanudan tras el reinicio." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Se instala y reinicia cuando terminen %lld agentes. Los agentes se reanudan tras el reinicio." + } + } + } + } + }, + "zh-Hant": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "將在 %lld 個代理完成後安裝並重新啟動。代理會在重新啟動後繼續。" + } + } + } + } + }, + "zh-Hans": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "将在 %lld 个代理完成后安装并重新启动。代理会在重新启动后继续。" + } + } + } + } + }, + "ko": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "에이전트 %lld개가 끝나면 설치하고 다시 시작합니다. 다시 시작한 후 에이전트가 재개됩니다." + } + } + } + } + }, + "ja": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld 個のエージェントが終了するとインストールして再起動します。エージェントは再起動後に再開されます。" + } + } + } + } + } + } + }, + "update.readyWaiting.commands": { + "extractionState": "manual", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Relaunching will stop %lld running command. Choose Install Now when you're ready." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Relaunching will stop %lld running commands. Choose Install Now when you're ready." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Der Neustart beendet %lld laufenden Befehl. Wähle „Jetzt installieren“, wenn du bereit bist." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Der Neustart beendet %lld laufende Befehle. Wähle „Jetzt installieren“, wenn du bereit bist." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Le redémarrage arrêtera %lld commande en cours. Choisissez Installer maintenant quand vous êtes prêt." + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "Le redémarrage arrêtera %lld de commandes en cours. Choisissez Installer maintenant quand vous êtes prêt." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Le redémarrage arrêtera %lld commandes en cours. Choisissez Installer maintenant quand vous êtes prêt." + } + } + } + } + }, + "ar": { + "variations": { + "plural": { + "zero": { + "stringUnit": { + "state": "translated", + "value": "ستؤدي إعادة التشغيل إلى إيقاف الأوامر قيد التشغيل (%lld). اختر التثبيت الآن عندما تكون جاهزًا." + } + }, + "one": { + "stringUnit": { + "state": "translated", + "value": "ستؤدي إعادة التشغيل إلى إيقاف أمر واحد قيد التشغيل (%lld). اختر التثبيت الآن عندما تكون جاهزًا." + } + }, + "two": { + "stringUnit": { + "state": "translated", + "value": "ستؤدي إعادة التشغيل إلى إيقاف أمرين قيد التشغيل (%lld). اختر التثبيت الآن عندما تكون جاهزًا." + } + }, + "few": { + "stringUnit": { + "state": "translated", + "value": "ستؤدي إعادة التشغيل إلى إيقاف %lld أوامر قيد التشغيل. اختر التثبيت الآن عندما تكون جاهزًا." + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "ستؤدي إعادة التشغيل إلى إيقاف %lld أمرًا قيد التشغيل. اختر التثبيت الآن عندما تكون جاهزًا." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "ستؤدي إعادة التشغيل إلى إيقاف %lld أمر قيد التشغيل. اختر التثبيت الآن عندما تكون جاهزًا." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "El reinicio detendrá %lld comando en ejecución. Elige Instalar ahora cuando estés listo." + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "El reinicio detendrá %lld de comandos en ejecución. Elige Instalar ahora cuando estés listo." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "El reinicio detendrá %lld comandos en ejecución. Elige Instalar ahora cuando estés listo." + } + } + } + } + }, + "zh-Hant": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "重新啟動將停止 %lld 個執行中的命令。準備好後請選擇「立即安裝」。" + } + } + } + } + }, + "zh-Hans": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "重新启动将停止 %lld 个正在运行的命令。准备好后请选择“立即安装”。" + } + } + } + } + }, + "ko": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "다시 시작하면 실행 중인 명령 %lld개가 중지됩니다. 준비되면 지금 설치를 선택하세요." + } + } + } + } + }, + "ja": { + "variations": { + "plural": { + "other": { + "stringUnit": { + "state": "translated", + "value": "再起動すると実行中の %lld 個のコマンドが停止します。準備ができたら「今すぐインストール」を選択してください。" + } + } + } + } + } + } } }, "version": "1.0" diff --git a/Sources/App/AppDelegateDetachedInspectorClose.swift b/Sources/App/AppDelegateDetachedInspectorClose.swift index 76ff9ad8795..3725827feb8 100644 --- a/Sources/App/AppDelegateDetachedInspectorClose.swift +++ b/Sources/App/AppDelegateDetachedInspectorClose.swift @@ -59,7 +59,7 @@ extension AppDelegate { func handleDetachedInspectorCloseShortcutOutsideMainContext(event: NSEvent) -> Bool { guard isCloseTabShortcutEventOrChordPrefix(event) else { return false } - let panels = allBrowserPanelsForInspectorWindowClose() + let panels = allLiveBrowserPanels() if matchConfiguredShortcut(event: event, action: .closeTab) { return closeDetachedInspectorWindowForCloseShortcut(event: event, panels: panels) } @@ -101,7 +101,7 @@ extension AppDelegate { allowFallback: Self.allowsWindowFallback(for: action) ) else { return false } - for panel in allBrowserPanelsForInspectorWindowClose() { + for panel in allLiveBrowserPanels() { if panel.closeDeveloperToolsFromDetachedInspectorWindowUserAction( window, source: "sendAction.\(NSStringFromSelector(action))" @@ -167,56 +167,6 @@ extension AppDelegate { return allowFallback ? (AppDelegate.shared?.shortcutRoutingActiveWindow ?? NSApp.keyWindow ?? NSApp.mainWindow) : nil } - func allBrowserPanelsForInspectorWindowClose() -> [BrowserPanel] { - var candidateManagers: [TabManager] = [] - var seenManagers = Set() - var panels: [BrowserPanel] = [] - var seenPanels = Set() - - func appendCandidate(_ manager: TabManager?) { - guard let manager else { return } - let identifier = ObjectIdentifier(manager) - guard seenManagers.insert(identifier).inserted else { return } - candidateManagers.append(manager) - } - - appendCandidate(tabManager) - for context in mainWindowContexts.values { - appendCandidate(context.tabManager) - } - for route in recoverableMainWindowRoutes() { - appendCandidate(route.tabManager) - } - - func appendPanel(_ panel: any Panel) { - guard let browserPanel = panel as? BrowserPanel else { return } - let identifier = ObjectIdentifier(browserPanel) - guard seenPanels.insert(identifier).inserted else { return } - panels.append(browserPanel) - } - - for manager in candidateManagers { - for workspace in manager.tabs { - for panel in workspace.panels.values { - appendPanel(panel) - } - // Workspace Docks keep their own panel store; include their - // browser panels so detached-inspector close routing and focus - // handoff cover Dock-hosted DevTools too. - workspace._dockSplit?.forEachPanel { _, panel in - appendPanel(panel) - } - } - } - for context in mainWindowContexts.values { - context.existingWindowDock()?.forEachPanel { _, panel in - appendPanel(panel) - } - } - - return panels - } - } extension NSWindow { diff --git a/Sources/App/BrowserHiddenWebViewMemoryBudgetCoordinator.swift b/Sources/App/BrowserHiddenWebViewMemoryBudgetCoordinator.swift new file mode 100644 index 00000000000..818508eef16 --- /dev/null +++ b/Sources/App/BrowserHiddenWebViewMemoryBudgetCoordinator.swift @@ -0,0 +1,63 @@ +import CmuxBrowser +import Darwin +import Foundation +import WebKit + +/// Keeps the memory held by hidden browser panes under the configured budget +/// by discarding the pane hidden longest first, like a Chrome tab discard +/// (https://github.com/manaflow-ai/cmux/issues/15069). Runs on each memory +/// monitor sample; the timer policy stays opt-in. +@MainActor +final class BrowserHiddenWebViewMemoryBudgetCoordinator { + private let policyDefaults: UserDefaults + private let browserPanels: @MainActor () -> [BrowserPanel] + private let processIdentifier: @MainActor (WKWebView) -> Int? + private let footprintBytes: (Int) -> UInt64? + + init( + policyDefaults: UserDefaults = .standard, + processIdentifier: @escaping @MainActor (WKWebView) -> Int? = { CmuxWebContentProcessIdentifier.pid(for: $0) }, + footprintBytes: @escaping (Int) -> UInt64? = { CmuxTopProcessSnapshot.resourceUsage(for: $0)?.ri_phys_footprint }, + browserPanels: @escaping @MainActor () -> [BrowserPanel] + ) { + self.policyDefaults = policyDefaults + self.processIdentifier = processIdentifier + self.footprintBytes = footprintBytes + self.browserPanels = browserPanels + } + + /// Discards hidden panes, hidden longest first, until the memory their + /// web content holds fits the budget. + /// + /// - Returns: The number of panes discarded. + @discardableResult + func enforceBudget(now: Date = Date()) -> Int { + let policy = BrowserHiddenWebViewDiscardPolicy.self + guard policy.isEnabled(defaults: policyDefaults), + policy.mode(defaults: policyDefaults) == .memoryBudget else { return 0 } + + let panels = browserPanels() + // Each sample runs on the main actor; with every pane on screen there + // is nothing to plan, so skip the per-pane checks and process reads. + guard panels.contains(where: { !$0.isWebViewVisibleInUI }) else { return 0 } + let panes = panels.map { $0.hiddenMemoryBudgetPane(now: now, processIdentifier: processIdentifier) } + let planner = BrowserHiddenWebViewMemoryBudgetPlanner(budgetMB: policy.memoryBudgetMB(defaults: policyDefaults)) + let plan = planner.plan(panes: panes, footprintBytes: footprintBytes) + guard !plan.panesToDiscard.isEmpty else { return 0 } + + let panelsByID = Dictionary(panels.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) + let discardedCount = plan.panesToDiscard.reduce(0) { count, paneID in + let discarded = panelsByID[paneID]?.discardHiddenWebViewForMemoryBudget(now: now) == true + return count + (discarded ? 1 : 0) + } +#if DEBUG + cmuxDebugLog( + "browser.discard.budget budgetBytes=\(planner.budgetBytes) " + + "hiddenBytes=\(plan.hiddenFootprintBytes) " + + "remainingBytes=\(plan.remainingHiddenFootprintBytes) " + + "planned=\(plan.panesToDiscard.count) discarded=\(discardedCount)" + ) +#endif + return discardedCount + } +} diff --git a/Sources/App/BrowserHiddenWebViewMemoryPressureResponder.swift b/Sources/App/BrowserHiddenWebViewMemoryPressureResponder.swift index c4a1fc23c97..769f2913201 100644 --- a/Sources/App/BrowserHiddenWebViewMemoryPressureResponder.swift +++ b/Sources/App/BrowserHiddenWebViewMemoryPressureResponder.swift @@ -6,17 +6,15 @@ final class BrowserHiddenWebViewMemoryPressureResponder: MemoryPressureResponder let memoryPressureMinimumSeverity: MemoryPressureSeverity = .warning let memoryPressurePriority = 90 - private let tabManagers: @MainActor () -> [TabManager] + private let browserPanels: @MainActor () -> [BrowserPanel] - init(tabManagers: @escaping @MainActor () -> [TabManager]) { - self.tabManagers = tabManagers + init(browserPanels: @escaping @MainActor () -> [BrowserPanel]) { + self.browserPanels = browserPanels } func shedMemory(for snapshot: MemoryPressureSnapshot) -> MemoryPressureShedResult { - let discardedCount = tabManagers().reduce(0) { count, manager in - count + manager.discardHiddenBrowserWebViewsForSystemMemoryPressure( - now: snapshot.sampledAt - ) + let discardedCount = browserPanels().reduce(0) { count, panel in + panel.discardHiddenWebViewForSystemMemoryPressure(now: snapshot.sampledAt) ? count + 1 : count } return MemoryPressureShedResult( reclaimedItemCount: discardedCount, diff --git a/Sources/App/BrowserInspectorFocusHandoff.swift b/Sources/App/BrowserInspectorFocusHandoff.swift index 5010fea0fb8..8856f9e734c 100644 --- a/Sources/App/BrowserInspectorFocusHandoff.swift +++ b/Sources/App/BrowserInspectorFocusHandoff.swift @@ -28,7 +28,7 @@ extension AppDelegate { } func browserPanelOwningInspectorResponder(_ responder: NSResponder) -> BrowserPanel? { - for browserPanel in browserPanelsForInspectorFocusHandoff() { + for browserPanel in allLiveBrowserPanels() { guard let frontendWebView = browserPanel.webView.cmuxInspectorFrontendWebView(), browserInspectorResponder(responder, belongsTo: frontendWebView) else { continue diff --git a/Sources/App/MemoryPressureMonitor.swift b/Sources/App/MemoryPressureMonitor.swift index cb4265e5bcc..2f30836189f 100644 --- a/Sources/App/MemoryPressureMonitor.swift +++ b/Sources/App/MemoryPressureMonitor.swift @@ -23,6 +23,9 @@ final class MemoryPressureMonitor { @ObservationIgnored var onAggregatePressureCleared: (@MainActor () -> Void)? + /// Runs after each applied sample, at any severity, with its time. + @ObservationIgnored + var onSampleApplied: (@MainActor (Date) -> Void)? @ObservationIgnored private let footprintSampler: any MemoryPressureFootprintSampling @@ -298,6 +301,7 @@ final class MemoryPressureMonitor { if evaluation.didBecomePersistentCritical { Self.logger.notice("memoryPressure.persistentCritical") } + onSampleApplied?(sampledAt) } private func logTransition(_ evaluation: MemoryPressureStateEvaluation) { diff --git a/Sources/AppDelegate+PaneMemoryGuardrail.swift b/Sources/AppDelegate+PaneMemoryGuardrail.swift index 89f8b540203..538a79487ec 100644 --- a/Sources/AppDelegate+PaneMemoryGuardrail.swift +++ b/Sources/AppDelegate+PaneMemoryGuardrail.swift @@ -33,7 +33,7 @@ extension AppDelegate { ) monitor.registry.register( BrowserHiddenWebViewMemoryPressureResponder { [weak self] in - self?.paneMemoryGuardrailTabManagers() ?? [] + self?.allLiveBrowserPanels() ?? [] } ) monitor.registry.register( @@ -61,9 +61,39 @@ extension AppDelegate { monitor.onAggregatePressureCleared = { AgentHibernationController.shared.clearAggregateMemoryPressureConfirmations() } + let browserMemoryBudget = BrowserHiddenWebViewMemoryBudgetCoordinator { [weak self] in + self?.allLiveBrowserPanels() ?? [] + } + monitor.onSampleApplied = { sampledAt in + browserMemoryBudget.enforceBudget(now: sampledAt) + } monitor.start() } + /// Every live browser panel, each once: workspace panes plus the panes of + /// workspace and window Docks. + func allLiveBrowserPanels() -> [BrowserPanel] { + var panels: [BrowserPanel] = [] + var seen: Set = [] + + func append(_ panel: any Panel) { + guard let browserPanel = panel as? BrowserPanel, + seen.insert(ObjectIdentifier(browserPanel)).inserted else { return } + panels.append(browserPanel) + } + + for manager in paneMemoryGuardrailTabManagers() { + for workspace in manager.tabs { + workspace.panels.values.forEach(append) + workspace._dockSplit?.forEachPanel { _, panel in append(panel) } + } + } + for dock in existingWindowDocks { + dock.forEachPanel { _, panel in append(panel) } + } + return panels + } + private func paneMemoryGuardrailTabManagers() -> [TabManager] { var managers: [TabManager] = [] var seen: Set = [] diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 64763aa742b..b3a7fd50923 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1773,12 +1773,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #if DEBUG // UI tests run on a shared VM user profile, so persisted shortcuts can drift and make - // key-equivalent routing flaky. Force defaults for deterministic tests. The same - // profile carries the last closed window's frame, which sizes the launch window. + // key-equivalent routing flaky. Force defaults for deterministic tests. App-host test + // processes already start from their own empty domain (TestProcessDefaults). if isRunningUnderXCTest { SystemWideHotkeySettings.reset() KeyboardShortcutSettings.resetAll() - Self.forgetPersistedWindowGeometryForTestProcess() + if TestProcessDefaults.isolatedDomainName == nil { + Self.forgetPersistedWindowGeometryForTestProcess() + } } #endif @@ -3860,23 +3862,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent legacyPersistedWindowGeometryDefaultsKeys.forEach { defaults.removeObjectIfPresent(forKey: $0) } } - /// Forgets the last closed main window's frame so a test process opens its - /// first window at the default size. - /// - /// Every main-window close writes its frame to the app's standard - /// defaults, and the launch window and any window created without a source - /// window read it back. App-host test processes on one machine share that - /// domain, so without this reset a process inherits whatever window an - /// earlier process closed last, often a 320-point fixture. Every later - /// `createMainWindow()` copies that launch window, and split admission then - /// refuses side-by-side splits (#15392). - nonisolated static func forgetPersistedWindowGeometryForTestProcess( - defaults: UserDefaults = .standard - ) { - removeLegacyPersistedWindowGeometry(defaults: defaults) - defaults.removeObjectIfPresent(forKey: persistedWindowGeometryDefaultsKey) - } - private func persistWindowGeometry(from window: NSWindow?) { guard let window else { return } persistWindowGeometry( @@ -15719,46 +15704,50 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // Workspace navigation: Cmd+Ctrl+] / Cmd+Ctrl+[ if matchConfiguredShortcut(event: event, action: .nextSidebarTab) { + let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager #if DEBUG - let selected = tabManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" + let selected = routedManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" cmuxDebugLog( "ws.shortcut dir=next repeat=\(event.isARepeat ? 1 : 0) keyCode=\(event.keyCode) selected=\(selected)" ) #endif - tabManager?.selectNextTab() + routedManager?.selectNextTab() return true } if matchConfiguredShortcut(event: event, action: .prevSidebarTab) { + let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager #if DEBUG - let selected = tabManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" + let selected = routedManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" cmuxDebugLog( "ws.shortcut dir=prev repeat=\(event.isARepeat ? 1 : 0) keyCode=\(event.keyCode) selected=\(selected)" ) #endif - tabManager?.selectPreviousTab() + routedManager?.selectPreviousTab() return true } if matchConfiguredShortcut(event: event, action: .nextSidebarTabInGroup) { + let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager #if DEBUG - let selected = tabManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" + let selected = routedManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" cmuxDebugLog( "ws.shortcut dir=next scope=group repeat=\(event.isARepeat ? 1 : 0) keyCode=\(event.keyCode) selected=\(selected)" ) #endif - tabManager?.selectNextTab(scope: .focusedGroupMembers) + routedManager?.selectNextTab(scope: .focusedGroupMembers) return true } if matchConfiguredShortcut(event: event, action: .prevSidebarTabInGroup) { + let routedManager = preferredMainWindowContextForShortcutRouting(event: event)?.tabManager ?? tabManager #if DEBUG - let selected = tabManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" + let selected = routedManager?.selectedTabId.map { String($0.uuidString.prefix(5)) } ?? "nil" cmuxDebugLog( "ws.shortcut dir=prev scope=group repeat=\(event.isARepeat ? 1 : 0) keyCode=\(event.keyCode) selected=\(selected)" ) #endif - tabManager?.selectPreviousTab(scope: .focusedGroupMembers) + routedManager?.selectPreviousTab(scope: .focusedGroupMembers) return true } @@ -15841,7 +15830,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // The Close Tab shortcut must close the focused panel even if first-responder // momentarily lags on a browser NSTextView during split focus transitions. if matchConfiguredShortcut(event: event, action: .closeTab) { - let panels = allBrowserPanelsForInspectorWindowClose() + let panels = allLiveBrowserPanels() if closeDetachedInspectorWindowForCloseShortcut(event: event, panels: panels) { return true } @@ -19476,11 +19465,6 @@ extension AppDelegate { } } -extension AppDelegate { - func browserPanelsForInspectorFocusHandoff() -> [BrowserPanel] { - allBrowserPanelsForInspectorWindowClose() - } -} private extension NSWindow { static func cmuxCommandPaletteOwnsFieldEditor(_ textView: NSTextView?, in window: NSWindow) -> Bool { guard let textView, diff --git a/Sources/BrowserAction.swift b/Sources/BrowserAction.swift index 8afb44c8026..9b6ed341ee5 100644 --- a/Sources/BrowserAction.swift +++ b/Sources/BrowserAction.swift @@ -15,6 +15,7 @@ enum BrowserAction { case showJavaScriptConsole case toggleReactGrab case toggleDesignMode(reason: String) + case toggleKeepPageActive case zoomIn case zoomOut case resetZoom diff --git a/Sources/BrowserActionDispatcher.swift b/Sources/BrowserActionDispatcher.swift index b16db3591a7..a870081d567 100644 --- a/Sources/BrowserActionDispatcher.swift +++ b/Sources/BrowserActionDispatcher.swift @@ -57,6 +57,9 @@ struct BrowserActionDispatcher { _ = await panel?.toggleDesignMode(reason: reason) } return true + case .toggleKeepPageActive: + panel.keepsPageActiveWhileHidden.toggle() + return true case .zoomIn: return panel.zoomIn() case .zoomOut: diff --git a/Sources/Cloud/CloudTreeExpansionPreferences.swift b/Sources/Cloud/CloudTreeExpansionPreferences.swift index da176e506e9..973c06f0100 100644 --- a/Sources/Cloud/CloudTreeExpansionPreferences.swift +++ b/Sources/Cloud/CloudTreeExpansionPreferences.swift @@ -9,7 +9,7 @@ struct CloudTreeExpansionPreferences: CloudTreeExpansionPersistence { private let applicationID: String? private let writer: CloudTreeExpansionPreferencesWriter - /// A nil domain passes Core Foundation's current-application sentinel intact. + /// A nil domain means the domain `UserDefaults.standard` uses. init(applicationID: String? = nil) { self.applicationID = applicationID writer = CloudTreeExpansionPreferencesWriter(applicationID: applicationID) @@ -32,6 +32,6 @@ struct CloudTreeExpansionPreferences: CloudTreeExpansionPersistence { func flush() async -> Bool { await writer.flush() } private var domain: CFString { - applicationID.map { $0 as CFString } ?? kCFPreferencesCurrentApplication + applicationID.map { $0 as CFString } ?? ProcessDefaultsDomain.cfApplicationID } } diff --git a/Sources/Cloud/CloudTreeExpansionPreferencesWriter.swift b/Sources/Cloud/CloudTreeExpansionPreferencesWriter.swift index f26438b3007..796123b3dce 100644 --- a/Sources/Cloud/CloudTreeExpansionPreferencesWriter.swift +++ b/Sources/Cloud/CloudTreeExpansionPreferencesWriter.swift @@ -8,6 +8,6 @@ actor CloudTreeExpansionPreferencesWriter { @discardableResult func flush() -> Bool { - CFPreferencesAppSynchronize(applicationID.map { $0 as CFString } ?? kCFPreferencesCurrentApplication) + CFPreferencesAppSynchronize(applicationID.map { $0 as CFString } ?? ProcessDefaultsDomain.cfApplicationID) } } diff --git a/Sources/CmuxMain.swift b/Sources/CmuxMain.swift index ac43daccbda..971831c7f78 100644 --- a/Sources/CmuxMain.swift +++ b/Sources/CmuxMain.swift @@ -18,6 +18,9 @@ import SwiftUI enum CmuxMain { /// Raises inherited descriptor limits before receipt writing or worker routing. static func main() { + // First: nothing may read preferences before an app-host test process + // switches to its own domain. + TestProcessDefaults.installIfHostingTests() FileDescriptorLimitController().raiseSoftLimitIfNeeded() AppHostProcessReceipt.writeIfRequired() #if DEBUG diff --git a/Sources/CmuxSettingsFileStore+Live.swift b/Sources/CmuxSettingsFileStore+Live.swift index ff42009e1b8..593a9ac1620 100644 --- a/Sources/CmuxSettingsFileStore+Live.swift +++ b/Sources/CmuxSettingsFileStore+Live.swift @@ -117,7 +117,7 @@ extension CmuxSettingsFileStore { /// Creates the process store wired to the host's shared reload coordinator. static var appLive: CmuxSettingsFileStore { CmuxSettingsFileStore( - languageSettingsStore: LanguageSettingsStore(defaults: .standard), + languageSettingsStore: LanguageSettingsStore(defaults: .standard, domainName: ProcessDefaultsDomain.name), onWatchedFileReload: { source in AppDelegate.shared?.reconcileSocketListenerConfiguration(source: source) } diff --git a/Sources/CmuxSettingsFileStore+SupportedPaths.swift b/Sources/CmuxSettingsFileStore+SupportedPaths.swift index 00b6fe8f366..c81f09ce48d 100644 --- a/Sources/CmuxSettingsFileStore+SupportedPaths.swift +++ b/Sources/CmuxSettingsFileStore+SupportedPaths.swift @@ -154,7 +154,10 @@ extension CmuxSettingsFileStore { "browser.showSearchSuggestions", "browser.theme", "browser.discardHiddenWebViews", + "browser.hiddenWebViewDiscardMode", + "browser.hiddenWebViewMemoryBudgetMB", "browser.hiddenWebViewDiscardDelaySeconds", + "browser.autoRestoreUnloadedPages", "browser.askWhereToSaveDownloads", "browser.openTerminalLinksInCmuxBrowser", "browser.interceptTerminalOpenCommandInCmuxBrowser", diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index ca8c20724b4..7ba85203c73 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -458,7 +458,7 @@ enum AutomationSettingsFileMapping { enum BrowserSettingsFileMapping { static let booleanSettings: [SettingsFileBooleanMapping] = [ .init(jsonKey: "showSearchSuggestions", defaultsKey: BrowserSearchSettingsStore.searchSuggestionsEnabledKey), - .init(jsonKey: "discardHiddenWebViews", defaultsKey: BrowserHiddenWebViewDiscardPolicy.enabledKey), + .init(jsonKey: "discardHiddenWebViews", defaultsKey: BrowserHiddenWebViewDiscardPolicy.enabledKey), .init(jsonKey: "autoRestoreUnloadedPages", defaultsKey: BrowserHiddenWebViewDiscardPolicy.autoRestoreKey), .init( jsonKey: "askWhereToSaveDownloads", defaultsKey: SettingCatalog().browser.askWhereToSaveDownloads.userDefaultsKey diff --git a/Sources/ContentView+ViewCommandPalette.swift b/Sources/ContentView+ViewCommandPalette.swift index 84b69524cf2..64a43abf0e8 100644 --- a/Sources/ContentView+ViewCommandPalette.swift +++ b/Sources/ContentView+ViewCommandPalette.swift @@ -91,6 +91,45 @@ extension ContentView { ) } + /// Sets the browser toggle states that palette titles reflect. + static func setCommandPaletteBrowserToggleContext( + for browserPanel: BrowserPanel, + in snapshot: inout CommandPaletteContextSnapshot + ) { + snapshot.setBool(CommandPaletteContextKeys.panelBrowserFocusModeActive, browserPanel.isBrowserFocusModeActive) + snapshot.setBool(CommandPaletteContextKeys.panelBrowserKeepsPageActive, browserPanel.keepsPageActiveWhileHidden) + } + + static func appendBrowserKeepPageActiveCommandContribution( + to contributions: inout [CommandPaletteCommandContribution], + panelSubtitle: @escaping (CommandPaletteContextSnapshot) -> String + ) { + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.browserKeepPageActive", + title: { context in + context.bool(CommandPaletteContextKeys.panelBrowserKeepsPageActive) + ? String(localized: "command.browserKeepPageActive.disable.title", defaultValue: "Allow Page to Unload While Hidden") + : String(localized: "command.browserKeepPageActive.enable.title", defaultValue: "Keep Page Active While Hidden") + }, + subtitle: panelSubtitle, + keywords: ["browser", "keep", "active", "pin", "memory", "unload", "discard", "background", "hidden"], + when: { $0.bool(CommandPaletteContextKeys.panelIsBrowser) } + ) + ) + } + + func registerBrowserKeepPageActiveCommandHandler( + _ registry: inout CommandPaletteHandlerRegistry, + performBrowserAction: @escaping (BrowserAction) -> Bool + ) { + registry.register(commandId: "palette.browserKeepPageActive") { + if !performBrowserAction(.toggleKeepPageActive) { + NSSound.beep() + } + } + } + func registerViewCommandHandlers(_ registry: inout CommandPaletteHandlerRegistry) { registry.register(commandId: "palette.triggerFlash") { tabManager.triggerFocusFlash() diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index c8ea47075b7..8487cedb832 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -7182,10 +7182,7 @@ struct ContentView: View { CommandPaletteContextKeys.panelIsBrowser, true ) - snapshot.setBool( - CommandPaletteContextKeys.panelBrowserFocusModeActive, - browserPanel.isBrowserFocusModeActive - ) + Self.setCommandPaletteBrowserToggleContext(for: browserPanel, in: &snapshot) snapshot.setBool( CommandPaletteContextKeys.panelBrowserOmnibarVisible, browserPanel.isOmnibarVisible @@ -7227,9 +7224,7 @@ struct ContentView: View { snapshot.setString(CommandPaletteContextKeys.panelName, panelDisplayName(workspace: workspace, panelId: panelId, fallback: panelContext.panel.displayTitle)) snapshot.setBool(CommandPaletteContextKeys.panelIsBrowser, panelContext.panel.panelType == .browser) snapshot.setBool(CommandPaletteContextKeys.panelIsSimulator, panelContext.panel.panelType == .simulator) - if let browserPanel = panelContext.panel as? BrowserPanel { - snapshot.setBool(CommandPaletteContextKeys.panelBrowserFocusModeActive, browserPanel.isBrowserFocusModeActive) - } + if let browserPanel = panelContext.panel as? BrowserPanel { Self.setCommandPaletteBrowserToggleContext(for: browserPanel, in: &snapshot) } // Markdown zoom only affects the rendered preview, so don't surface // the zoom commands when the panel is in raw text-edit mode. snapshot.setBool( @@ -8131,6 +8126,7 @@ struct ContentView: View { when: { $0.bool(CommandPaletteContextKeys.panelIsBrowser) } ) ) + Self.appendBrowserKeepPageActiveCommandContribution(to: &contributions, panelSubtitle: browserPanelSubtitle) Self.appendViewZoomCommandContributions(to: &contributions, panelSubtitle: panelSubtitle) contributions.append( CommandPaletteCommandContribution( @@ -9469,6 +9465,7 @@ struct ContentView: View { } } registerPaneResizeHandlers(®istry) { observedWindow ?? NSApp.keyWindow ?? NSApp.mainWindow } + registerBrowserKeepPageActiveCommandHandler(®istry, performBrowserAction: performBrowserAction) registerShortcutParityCommandHandlers( ®istry, performBrowserAction: performBrowserAction diff --git a/Sources/DockSplitStore+SessionSnapshot.swift b/Sources/DockSplitStore+SessionSnapshot.swift index 71e017bd215..c85de80edc0 100644 --- a/Sources/DockSplitStore+SessionSnapshot.swift +++ b/Sources/DockSplitStore+SessionSnapshot.swift @@ -386,7 +386,7 @@ extension DockSplitStore { forwardHistoryURLStrings: history.forwardHistoryURLStrings, transparentBackground: browser.sessionSnapshotTransparentBackground, diffViewerToken: diffViewer?.token, - diffViewerRequestPath: diffViewer?.requestPath, cloudResource: browser.cloudResourceForSession, + diffViewerRequestPath: diffViewer?.requestPath, cloudResource: browser.cloudResourceForSession, interactionState: browser.persistableInteractionStateForSessionSnapshot(), keepsPageActive: browser.keepsPageActiveForSessionSnapshot, cloudTeamID: browser.cloudTeamIDForSession ) } else if let deferred = panel as? DeferredBrowserPanel { diff --git a/Sources/GhosttyScrollView.swift b/Sources/GhosttyScrollView.swift index 61f7c3c9d05..95b1fd86c63 100644 --- a/Sources/GhosttyScrollView.swift +++ b/Sources/GhosttyScrollView.swift @@ -209,8 +209,8 @@ final class GhosttyScrollView: NSScrollView { nonisolated static func savedShowScrollBarsPreference() -> String? { let key = TerminalScrollerStyle.showScrollBarsDefaultsKey let defaults = UserDefaults.standard - if let bundleIdentifier = Bundle.main.bundleIdentifier, - let appValue = defaults.persistentDomain(forName: bundleIdentifier)?[key] as? String { + if let domainName = ProcessDefaultsDomain.name, + let appValue = defaults.persistentDomain(forName: domainName)?[key] as? String { return appValue } return defaults.persistentDomain(forName: UserDefaults.globalDomain)?[key] as? String diff --git a/Sources/HostSettingsActions.swift b/Sources/HostSettingsActions.swift index 36e8a1f8721..d24541b0f1f 100644 --- a/Sources/HostSettingsActions.swift +++ b/Sources/HostSettingsActions.swift @@ -130,7 +130,7 @@ final class HostSettingsActions: SettingsHostActions { } func resetAllSettingsSideEffects() { - LanguageSettingsStore(defaults: .standard).applyLanguageOverride(.system) + LanguageSettingsStore(defaults: .standard, domainName: ProcessDefaultsDomain.name).applyLanguageOverride(.system) PaneChromeSettings.notifyDidChange() TerminalAdaptiveDefaultThemeSettings.notifyDidChange() PhonePushClient.shared.reloadConfigurationFromDefaults() @@ -249,7 +249,7 @@ final class HostSettingsActions: SettingsHostActions { } func applyLanguageOverride(_ language: AppLanguage) { - LanguageSettingsStore(defaults: .standard).applyLanguageOverride(language) + LanguageSettingsStore(defaults: .standard, domainName: ProcessDefaultsDomain.name).applyLanguageOverride(language) } func openConfigInExternalEditor() { diff --git a/Sources/KeyboardShortcutSettingsFileStore+BrowserMemorySaver.swift b/Sources/KeyboardShortcutSettingsFileStore+BrowserMemorySaver.swift new file mode 100644 index 00000000000..97dfb8d7bd7 --- /dev/null +++ b/Sources/KeyboardShortcutSettingsFileStore+BrowserMemorySaver.swift @@ -0,0 +1,42 @@ +import CmuxBrowser +import CmuxSettings +import Foundation + +/// Settings-file parsing for the browser Memory Saver keys, extracted from +/// `KeyboardShortcutSettingsFileStore.swift`, which sits at its file-length budget. +extension CmuxSettingsFileStore { + /// Applies `browser.hiddenWebViewDiscardMode`, `browser.hiddenWebViewMemoryBudgetMB` + /// and `browser.hiddenWebViewDiscardDelaySeconds`. Returns `false` for an invalid + /// delay so the caller keeps its existing behavior of skipping the rest of the + /// browser section. + func parseBrowserMemorySaverSettings( + _ section: [String: Any], + sourcePath: String, + snapshot: inout ResolvedSettingsSnapshot + ) -> Bool { + if section.keys.contains("hiddenWebViewDiscardMode") { + if let raw = jsonString(section["hiddenWebViewDiscardMode"]), + let mode = BrowserHiddenWebViewDiscardMode(rawValue: raw) { + snapshot.managedUserDefaults[BrowserHiddenWebViewDiscardPolicy.modeKey] = .string(mode.rawValue) + } else { + logInvalid("browser.hiddenWebViewDiscardMode", sourcePath: sourcePath) + } + } + if section.keys.contains("hiddenWebViewMemoryBudgetMB") { + if let value = jsonInt(section["hiddenWebViewMemoryBudgetMB"]), + let budget = BrowserHiddenWebViewDiscardPolicy.resolvedMemoryBudgetMB(value) { + snapshot.managedUserDefaults[BrowserHiddenWebViewDiscardPolicy.memoryBudgetKey] = .int(budget) + } else { + logInvalid("browser.hiddenWebViewMemoryBudgetMB", sourcePath: sourcePath) + } + } + if let value = jsonDouble(section["hiddenWebViewDiscardDelaySeconds"]) { + guard let delay = BrowserHiddenWebViewDiscardPolicy.resolvedHiddenDelay(value) else { + logInvalid("browser.hiddenWebViewDiscardDelaySeconds", sourcePath: sourcePath) + return false + } + snapshot.managedUserDefaults[BrowserHiddenWebViewDiscardPolicy.hiddenDelayKey] = .double(delay) + } + return true + } +} diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index ec0d90ae41b..d961d9db560 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -226,7 +226,10 @@ extension CmuxSettingsFileStore { "showSearchSuggestions": BrowserSearchSettingsStore.defaultSearchSuggestionsEnabled, "theme": BrowserThemeSettings.defaultMode.rawValue, "discardHiddenWebViews": BrowserHiddenWebViewDiscardPolicy.defaultEnabled, + "hiddenWebViewDiscardMode": BrowserHiddenWebViewDiscardPolicy.defaultMode.rawValue, + "hiddenWebViewMemoryBudgetMB": BrowserHiddenWebViewDiscardPolicy.defaultMemoryBudgetMB, "hiddenWebViewDiscardDelaySeconds": BrowserHiddenWebViewDiscardPolicy.defaultHiddenDelay, + "autoRestoreUnloadedPages": BrowserHiddenWebViewDiscardPolicy.defaultAutoRestore, "askWhereToSaveDownloads": SettingCatalog().browser.askWhereToSaveDownloads.defaultValue, "openTerminalLinksInCmuxBrowser": BrowserLinkOpenSettings.defaultOpenTerminalLinksInCmuxBrowser, "interceptTerminalOpenCommandInCmuxBrowser": BrowserLinkOpenSettings.defaultInterceptTerminalOpenCommandInCmuxBrowser, diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 505df5cba19..7f61a0184f2 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -916,13 +916,7 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[BrowserThemeSettings.modeKey] = .string(mode.rawValue) } - if let value = jsonDouble(section["hiddenWebViewDiscardDelaySeconds"]) { - guard let delay = BrowserHiddenWebViewDiscardPolicy.resolvedHiddenDelay(value) else { - logInvalid("browser.hiddenWebViewDiscardDelaySeconds", sourcePath: sourcePath) - return - } - snapshot.managedUserDefaults[BrowserHiddenWebViewDiscardPolicy.hiddenDelayKey] = .double(delay) - } + _ = parseBrowserMemorySaverSettings(section, sourcePath: sourcePath, snapshot: &snapshot) applyNormalizedStringArraySettings(BrowserSettingsFileMapping.stringArraySettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) } diff --git a/Sources/MemoryResourceViewCounts.swift b/Sources/MemoryResourceViewCounts.swift index ab2f3492809..366a48ea400 100644 --- a/Sources/MemoryResourceViewCounts.swift +++ b/Sources/MemoryResourceViewCounts.swift @@ -10,7 +10,7 @@ struct MemoryResourceViewCounts: Sendable { @MainActor static func capture() -> Self { let surfaces = GhosttyApp.terminalSurfaceRegistry.allTerminalSurfacesUnordered() - let browsers = AppDelegate.shared?.browserPanelsForInspectorFocusHandoff() ?? [] + let browsers = AppDelegate.shared?.allLiveBrowserPanels() ?? [] return Self( visibleTerminals: surfaces.filter(\.isRendererEffectivelyVisible).count, realizedRenderers: surfaces.filter(\.isRendererRealized).count, diff --git a/Sources/Panels/BrowserDiscardRestoreHeal.swift b/Sources/Panels/BrowserDiscardRestoreHeal.swift index e83254c1e0b..6b80d0e19ec 100644 --- a/Sources/Panels/BrowserDiscardRestoreHeal.swift +++ b/Sources/Panels/BrowserDiscardRestoreHeal.swift @@ -9,6 +9,7 @@ extension BrowserPanel { } func noteDiscardedWebViewRestoreNavigationStarted() { + pageRestoration.noteNavigationStarted() if hiddenWebViewDiscardManager.isDiscardedForMemory { // Each restore attempt tracks its own commit. Without this reset, a // previous attempt's error-page commit would satisfy the stall @@ -32,6 +33,7 @@ extension BrowserPanel { func noteDiscardedWebViewRestoreNavigationDidNotCommit(reason: String) { hiddenWebViewDiscardManager.noteRestoreNavigationDidNotCommit(reason: reason) pendingDiscardRestoreNavigation = nil + pageRestoration.dismissOverlay() refreshWebViewLifecycleState() } @@ -47,12 +49,23 @@ extension BrowserPanel { return navigation === tracked } + /// What asked for a discarded page to come back. + enum DiscardRestoreTrigger { + /// The pane became visible. With automatic restore off, the page + /// waits for the user instead. + case paneShown + /// The user or an automation needs the page now. + case explicitRequest + } + /// Restore touch for a possibly-discarded pane: detects stalled restore /// attempts, honors an explicit user Stop, restores through the discard - /// manager, and falls back to blank-shell healing. + /// manager (a pane whose process died while hidden restores the same + /// way), and falls back to blank-shell healing. @discardableResult func restoreDiscardedWebViewIfNeeded( reason: String, + trigger: DiscardRestoreTrigger = .explicitRequest, cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, allowBlankShellHeal: Bool = true, forceRestartPendingRestore: Bool = false @@ -70,6 +83,9 @@ extension BrowserPanel { if forceRestartPendingRestore { userStoppedLoadSinceWebViewReplacement = false } + // A process that died while hidden left no live page for Stop to + // keep; dropping its web view clears Stop. + discardWebViewTerminatedWhileHidden() // Stop is sticky for discarded restores too: routine visibility touches // must not restart a stopped load; explicit reload is the override. guard !userStoppedLoadSinceWebViewReplacement else { return false } @@ -86,15 +102,12 @@ extension BrowserPanel { guard let restoreURL, !Self.isAboutBlankURL(restoreURL) else { return reactivateDiscardedPaneWithoutRestorableURL(reason: reason) } + if trigger == .paneShown, hiddenWebViewDiscardManager.waitsForManualRestore { + return false + } if hiddenWebViewDiscardManager.restoreIfNeeded(reason: reason, force: forceRestartPendingRestore, performRestore: { - shouldRenderWebView = true - navigateWithoutInsecureHTTPPrompt( - to: restoreURL, - recordTypedNavigation: false, - preserveRestoredSessionHistory: true, - cachePolicy: cachePolicy - ) + performDiscardRestore(to: restoreURL, cachePolicy: cachePolicy, isExplicitReload: forceRestartPendingRestore) }) { return true } diff --git a/Sources/Panels/BrowserNavigationDelegate.swift b/Sources/Panels/BrowserNavigationDelegate.swift index d5bff0115c2..f98bd25c9d0 100644 --- a/Sources/Panels/BrowserNavigationDelegate.swift +++ b/Sources/Panels/BrowserNavigationDelegate.swift @@ -602,7 +602,6 @@ import WebKit decisionHandler(.cancel) return } - if navigationAction.targetFrame == nil, browserNavigationShouldFallbackNilTargetToNewTab( navigationType: navigationAction.navigationType @@ -628,7 +627,6 @@ import WebKit if navigationAction.targetFrame?.isMainFrame != false { if shouldPreserveSSLTrustBypassForErrorPageNavigation(navigationAction) { #if DEBUG - let targetURL = navigationAction.request.url?.absoluteString ?? "nil" cmuxDebugLog("browser.nav.decidePolicy.action kind=preserveSSLBypassErrorPage url=\(targetURL)") #endif } else if let url = navigationAction.request.url, @@ -654,8 +652,8 @@ import WebKit // accepted main-frame action while the bounded file probe runs so // other navigation policy branches remain synchronous. let encodingPolicy = owner.localFileEncodingPolicy - Task { @MainActor [weak owner, weak webView, encodingPolicy] in - guard let owner, let webView, + Task { @MainActor [weak owner, weak webView, weak self, encodingPolicy] in + guard let owner, let webView, let self, owner.webView === webView else { decisionHandler(.cancel) return @@ -672,10 +670,12 @@ import WebKit decisionHandler(.cancel) return } + self.recordAllowedNavigationRequest(navigationAction) decisionHandler(.allow) } return } + recordAllowedNavigationRequest(navigationAction) decisionHandler(.allow) } diff --git a/Sources/Panels/BrowserPageRecoveryOverlay.swift b/Sources/Panels/BrowserPageRecoveryOverlay.swift new file mode 100644 index 00000000000..a73afe0384a --- /dev/null +++ b/Sources/Panels/BrowserPageRecoveryOverlay.swift @@ -0,0 +1,79 @@ +import AppKit +import CmuxBrowser +import CmuxFoundation +import SwiftUI + +/// What a browser pane asks the user to do before its page comes back. +enum BrowserPageRecoveryPrompt { + /// The visible page's WebContent process died. + case crashed + /// The page was unloaded to save memory and automatic restore is off + /// (https://github.com/manaflow-ai/cmux/issues/9561). The snapshot is the + /// page as it looked when the pane was hidden. + case unloaded(snapshot: BrowserPageSnapshotImage?) +} + +/// Covers a browser pane whose page is gone until the user brings it back. +struct BrowserPageRecoveryOverlay: View { + let prompt: BrowserPageRecoveryPrompt + let backgroundColor: NSColor + let action: () -> Void + + var body: some View { + switch prompt { + case .crashed: + ZStack { + Color(nsColor: backgroundColor) + .opacity(0.92) + Button(action: action) { + Label( + String(localized: "browser.error.reload", defaultValue: "Reload"), + systemImage: "arrow.clockwise" + ) + .cmuxFont(size: 13, weight: .medium) + .padding(.horizontal, 6) + } + .buttonStyle(.borderedProminent) + .controlSize(.regular) + .safeHelp(String(localized: "browser.reload", defaultValue: "Reload")) + .accessibilityIdentifier("BrowserWebContentRecoveryButton") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + case .unloaded(let snapshot): + ZStack { + Color(nsColor: backgroundColor) + if let snapshot, let image = NSImage(data: snapshot.jpegData) { + Image(nsImage: image) + .resizable() + .frame(width: snapshot.pointSize.width, height: snapshot.pointSize.height) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .opacity(0.35) + .accessibilityHidden(true) + } + VStack(spacing: 10) { + Text(String( + localized: "browser.discard.unloadedMessage", + defaultValue: "This page was unloaded to save memory." + )) + .cmuxFont(size: 13) + .foregroundStyle(.secondary) + Button(action: action) { + Label( + String(localized: "browser.discard.restore", defaultValue: "Restore"), + systemImage: "arrow.counterclockwise" + ) + .cmuxFont(size: 13, weight: .medium) + .padding(.horizontal, 6) + } + .buttonStyle(.borderedProminent) + .controlSize(.regular) + .accessibilityIdentifier("BrowserUnloadedPageRestoreButton") + } + .padding(16) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + } + } +} diff --git a/Sources/Panels/BrowserPanel+AutomationRecovery.swift b/Sources/Panels/BrowserPanel+AutomationRecovery.swift index 3ce4e74467a..5083468b4d6 100644 --- a/Sources/Panels/BrowserPanel+AutomationRecovery.swift +++ b/Sources/Panels/BrowserPanel+AutomationRecovery.swift @@ -228,6 +228,20 @@ extension BrowserPanel { activeVisualAutomationCaptureCount == 0 } + /// The web view a browser automation command should drive. A pane whose WebContent process + /// died while hidden becomes a discarded pane first, as showing it would, so the command + /// captures the web view its document-readiness wait restores rather than a dead one only a + /// reveal could replace. The command also counts as use of the pane, so hidden-pane discards + /// do not unload a page an agent is driving. + func webViewForAutomationCommand(now: Date = Date()) -> WKWebView { + webViewLastAutomationCommandAt = now + discardWebViewTerminatedWhileHidden(now: now) + if !isWebViewVisibleInUI { + reevaluateHiddenWebViewDiscardScheduling(reason: "automation_command") + } + return webView + } + func waitForAutomationDocumentCommit( expectedWebViewIdentifier: ObjectIdentifier ) async -> BrowserAutomationDocumentReadinessOutcome { diff --git a/Sources/Panels/BrowserPanel+FormStateTracking.swift b/Sources/Panels/BrowserPanel+FormStateTracking.swift new file mode 100644 index 00000000000..c1a92bed8e0 --- /dev/null +++ b/Sources/Panels/BrowserPanel+FormStateTracking.swift @@ -0,0 +1,47 @@ +import CmuxBrowser +import Foundation +import WebKit + +extension BrowserPanel { + /// Main-frame observer that reports unsaved form input, which a discarded + /// pane restores after its page comes back. + static func installFormStateUserScript(into configuration: WKWebViewConfiguration) { + configuration.userContentController.addUserScript(.browserFormStateObserver()) + } + + func setupFormStateMessageHandler(for webView: WKWebView) { + // The handler outlives this web view generation on the old content + // controller, so reports from a replaced web view are ignored. + let boundWebViewInstanceID = webViewInstanceID + let handler = BrowserFormStateMessageHandler(webView: webView) { [weak self] snapshot in + guard let self, boundWebViewInstanceID == self.webViewInstanceID else { return } + let hadUnrestorableInput = self.pageRestoration.hasUnrestorableLiveInput + self.pageRestoration.recordLiveFormState(snapshot) + // Input a restore would lose keeps a hidden pane from a routine + // discard, so a timer armed without it must be re-evaluated. + if self.pageRestoration.hasUnrestorableLiveInput != hadUnrestorableInput { + self.reevaluateHiddenWebViewDiscardScheduling(reason: "form_input_changed") + } + } + pageRestoration.formStateMessageHandler = handler + webView.configuration.userContentController.addBrowserFormStateHandler(handler) + } + + func tearDownFormStateMessageHandler(for webView: WKWebView) { + webView.configuration.userContentController.removeBrowserFormStateHandler() + pageRestoration.formStateMessageHandler = nil + } + + /// Refills the restored document's unsaved input once it has loaded. + func applyPendingFormRestore(to webView: WKWebView) { + guard let formState = pageRestoration.takePendingFormRestore(for: webView.url) else { return } + webView.restoreBrowserFormState(formState) { error in + self.pageRestoration.retryPendingFormRestore(formState) +#if DEBUG + cmuxDebugLog("browser.discard.formRestore failed error=\(error.localizedDescription)") +#else + _ = error +#endif + } + } +} diff --git a/Sources/Panels/BrowserPanel+HiddenMemoryBudget.swift b/Sources/Panels/BrowserPanel+HiddenMemoryBudget.swift new file mode 100644 index 00000000000..276124967fb --- /dev/null +++ b/Sources/Panels/BrowserPanel+HiddenMemoryBudget.swift @@ -0,0 +1,52 @@ +import CmuxBrowser +import Foundation +import WebKit + +extension BrowserPanel { + /// Whether the user pinned this pane's page to stay live while hidden. + /// A pinned page is never discarded, even under system memory pressure. + var keepsPageActiveWhileHidden: Bool { + get { hiddenWebViewDiscardManager.keepsPageActive } + set { hiddenWebViewDiscardManager.keepsPageActive = newValue } + } + + /// The pin as the session file stores it: omitted when not pinned. + var keepsPageActiveForSessionSnapshot: Bool? { + keepsPageActiveWhileHidden ? true : nil + } + + /// When the hidden pane was last in use: when it was hidden, or when an + /// automation command last drove it, whichever is later. Hidden-pane + /// discards count their delay from here and evict the longest idle first. + var hiddenWebViewIdleSince: Date? { + guard let hiddenAt = webViewLastHiddenAt else { return nil } + return max(hiddenAt, webViewLastAutomationCommandAt ?? hiddenAt) + } + + /// This pane as the memory budget sees it. A pane with no live WebContent + /// process, because it was discarded or its process died, holds no memory. + func hiddenMemoryBudgetPane( + now: Date, + processIdentifier: (WKWebView) -> Int? + ) -> BrowserHiddenWebViewMemoryBudgetPlanner.Pane { + let hasLiveProcess = !hiddenWebViewDiscardManager.isDiscardedForMemory + && !hasRecoverableWebContentTermination + return BrowserHiddenWebViewMemoryBudgetPlanner.Pane( + id: id, + processID: hasLiveProcess ? processIdentifier(webView) : nil, + isVisible: isWebViewVisibleInUI, + hiddenAt: hiddenWebViewIdleSince, + isEvictable: hiddenWebViewDiscardManager.isEligibleForMemoryBudgetDiscard(now: now) + ) + } + + /// Discards this hidden pane to bring hidden web content back under the + /// memory budget, if nothing protects it. + /// + /// - Returns: Whether the pane was discarded. + @discardableResult + func discardHiddenWebViewForMemoryBudget(now: Date = Date()) -> Bool { + hiddenWebViewDiscardManager.requestMemoryBudgetDiscard(now: now) + && hiddenWebViewDiscardManager.isDiscardedForMemory + } +} diff --git a/Sources/Panels/BrowserPanel+MediaPlayback.swift b/Sources/Panels/BrowserPanel+MediaPlayback.swift index 00c2a068414..76223af8ac2 100644 --- a/Sources/Panels/BrowserPanel+MediaPlayback.swift +++ b/Sources/Panels/BrowserPanel+MediaPlayback.swift @@ -17,7 +17,8 @@ extension BrowserPanel { static let mediaPlaybackContentWorld = WKContentWorld.world(name: mediaPlaybackMessageHandlerName) /// Injected document-start hook that reports whether the current frame has - /// actively-playing and audible `