diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 8ea6ed0bb89c..8a7508ef1f00 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -20780,6 +20780,11 @@ struct CMUXCLI { cmux right-sidebar set find cmux right-sidebar set custom panel-info cmux right-sidebar mode + """) + "\n\n" + String(localized: "cli.rightSidebar.usage.find", defaultValue: """ + Find: + set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus] + Search Find for as if typed into its field + find-status Print the Find query, phase, results and files as JSON """) case "sidebar": return String(localized: "cli.sidebar.usage", defaultValue: """ @@ -21269,6 +21274,8 @@ struct CMUXCLI { let workspace: String? let window: String? let noFocus: Bool + /// `--query`, `--regex`, `--case-sensitive`, `--whole-word` for Find. + var findOptions: [String] = [] } private func forwardRightSidebarCommand( @@ -21293,7 +21300,8 @@ struct CMUXCLI { .map(shellQuote) .joined(separator: " ") let response = try sendV1Command(command, client: client) - if parsed.positional.first?.lowercased() == "mode" { + let action = parsed.positional.first?.lowercased() + if action == "mode" || action == "find-status" || action == "find_status" { print(response) } } @@ -21478,11 +21486,21 @@ struct CMUXCLI { var workspace: String? var window: String? var noFocus = false + var findOptions: [String] = [] var index = 0 while index < args.count { let arg = args[index] switch arg { + case "--query": + guard index + 1 < args.count else { + throw CLIError(message: String(localized: "cli.rightSidebar.error.queryRequiresValue", defaultValue: "right-sidebar: --query requires text")) + } + findOptions.append("--query=\(args[index + 1])") + index += 2 + case "--regex", "--case-sensitive", "--whole-word": + findOptions.append(arg) + index += 1 case "--workspace": guard index + 1 < args.count else { throw CLIError(message: String(localized: "cli.rightSidebar.error.workspaceRequiresValue", defaultValue: "right-sidebar: --workspace requires an id")) @@ -21499,7 +21517,10 @@ struct CMUXCLI { noFocus = true index += 1 default: - if arg.hasPrefix("--workspace=") { + if arg.hasPrefix("--query=") { + findOptions.append(arg) + index += 1 + } else if arg.hasPrefix("--workspace=") { workspace = String(arg.dropFirst("--workspace=".count)) index += 1 } else if arg.hasPrefix("--window=") { @@ -21517,7 +21538,8 @@ struct CMUXCLI { positional: positional, workspace: workspace, window: window, - noFocus: noFocus + noFocus: noFocus, + findOptions: findOptions ) } private func rightSidebarSocketArguments(from parsed: RightSidebarCLIArguments) throws -> [String] { @@ -21525,6 +21547,25 @@ struct CMUXCLI { throw CLIError(message: String(localized: "cli.rightSidebar.error.missingCommand", defaultValue: "right-sidebar requires a subcommand")) } + if action == "find-status" || action == "find_status" { + guard parsed.positional.count == 1, !parsed.noFocus, parsed.findOptions.isEmpty else { + throw CLIError(message: String(localized: "cli.rightSidebar.error.unexpectedArguments", defaultValue: "right-sidebar \(action) received unexpected arguments")) + } + return ["find_status"] + } + if !parsed.findOptions.isEmpty { + let modeArgument = action == "set" && parsed.positional.count == 2 ? parsed.positional[1] : action + guard normalizedRightSidebarCLIArgument(modeArgument) == "find", + parsed.positional.count == (action == "set" ? 2 : 1), + parsed.findOptions.contains(where: { $0.hasPrefix("--query=") }) else { + throw CLIError(message: String(localized: "cli.rightSidebar.error.findOptionsRequireFind", defaultValue: "right-sidebar: --query, --regex, --case-sensitive and --whole-word need 'find' or 'set find' and a --query")) + } + guard action == "set" || !parsed.noFocus else { + throw CLIError(message: String(localized: "cli.rightSidebar.error.noFocusOnlySet", defaultValue: "right-sidebar: --no-focus is only valid with set")) + } + return ["set", "find"] + parsed.findOptions + (parsed.noFocus ? ["--no-focus"] : []) + } + switch action { case "toggle", "show", "hide", "focus", "mode": guard parsed.positional.count == 1 else { diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarReportsV1.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarReportsV1.swift index 6e7caa851881..9f1f1a29e9a0 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarReportsV1.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlCommandCoordinator+SidebarReportsV1.swift @@ -508,6 +508,17 @@ extension ControlCommandCoordinator { "visible": .bool(visible), "mode": .string(modeRawValue), ])) + case .findStatus(let status): + return ControlResponseEncoder().encode(.object([ + "query": .string(status.query), + "regex": .bool(status.isRegex), + "case_sensitive": .bool(status.isCaseSensitive), + "whole_word": .bool(status.matchesWholeWord), + "phase": .string(status.phase), + "results": .int(Int64(status.results)), + "files": .int(Int64(status.files)), + "message": status.message.map { .string($0) } ?? .null, + ])) case .failure(let message): return message } diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarRightSidebarResolution.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarRightSidebarResolution.swift index 65c0340d9f66..b4ddf1548f06 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarRightSidebarResolution.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Sidebar/ControlSidebarRightSidebarResolution.swift @@ -8,7 +8,44 @@ public enum ControlSidebarRightSidebarResolution: Sendable, Equatable { case ok /// A `get`-style command returned sidebar state to encode. case state(visible: Bool, modeRawValue: String) + /// `find_status` returned the Find query and totals to encode. + case findStatus(ControlSidebarFindStatus) /// A parse or apply failure; `message` is the full legacy reply line /// (localized app-side where the original was localized). case failure(message: String) } + +/// The Find state `right_sidebar find_status` reports, so a script can wait +/// for a search and assert its totals. +public struct ControlSidebarFindStatus: Sendable, Equatable { + public let query: String + public let isRegex: Bool + public let isCaseSensitive: Bool + public let matchesWholeWord: Bool + /// `idle`, `searching`, `completed`, `limited` or `failed`. + public let phase: String + public let results: Int + public let files: Int + /// The status line as shown, or nil when hidden. + public let message: String? + + public init( + query: String, + isRegex: Bool, + isCaseSensitive: Bool, + matchesWholeWord: Bool, + phase: String, + results: Int, + files: Int, + message: String? + ) { + self.query = query + self.isRegex = isRegex + self.isCaseSensitive = isCaseSensitive + self.matchesWholeWord = matchesWholeWord + self.phase = phase + self.results = results + self.files = files + self.message = message + } +} diff --git a/Packages/macOS/CmuxFileSearch/Package.swift b/Packages/macOS/CmuxFileSearch/Package.swift new file mode 100644 index 000000000000..34807964d05d --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CmuxFileSearch", + platforms: [ + .macOS(.v14), + ], + products: [ + .library( + name: "CmuxFileSearch", + targets: ["CmuxFileSearch"] + ), + ], + targets: [ + .target( + name: "CmuxFileSearch", + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + ] + ), + .testTarget( + name: "CmuxFileSearchTests", + dependencies: ["CmuxFileSearch"], + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + ] + ), + ] +) diff --git a/Packages/macOS/CmuxFileSearch/README.md b/Packages/macOS/CmuxFileSearch/README.md new file mode 100644 index 000000000000..8b3f918f1599 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/README.md @@ -0,0 +1,27 @@ +# CmuxFileSearch + +Project-wide search for the right sidebar's Find mode, independent of AppKit. + +- `FileSearchQuery` holds the VS Code style options (Match Case, Match Whole + Word, Use Regular Expression, include and exclude globs, Use Exclude Settings + and Ignore Files). `FileSearchQuery.ripgrepArguments(rootPath:)` maps it to one `rg --json` argument + vector that runs unchanged locally, over SSH and on a Cloud VM. +- `RipgrepJSONLineParser` and `RipgrepStreamDecoder` turn ripgrep output into + one `FileSearchMatch` per submatch, with UTF-16 columns and a bounded preview + around the match. Invalid UTF-8 (`"bytes"` payloads) is decoded consistently. +- `FileSearchProcess` spawns the command with `posix_spawn` in its own process + group, so arguments are not Unicode-normalized and cancellation also stops + processes the command started. +- `FileSearchEngine` debounces requests with an injected `Clock`, cancels + superseded searches, and applies streamed batches to `FileSearchResultTree` + at most once per frame. `FileSearchRowList` flattens the tree into table + rows: `NSOutlineView` expansion is linear in the rows below each expanded + item, which measured several seconds for 100,000 streamed matches, while + appending table rows costs microseconds. + +```bash +swift test --package-path Packages/macOS/CmuxFileSearch +``` + +`FileSearchPerformanceTests` prints decode and tree timings for 100,000 +matches. The ripgrep end-to-end suite runs only when `rg` is installed. diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchBackend.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchBackend.swift new file mode 100644 index 000000000000..535e79c5ac37 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchBackend.swift @@ -0,0 +1,178 @@ +import Foundation + +/// How a search ended. +public enum FileSearchCompletion: Hashable, Sendable { + case completed + /// More than `limit` matches existed; the first `limit` are shown. + case limited(Int) + case failed(FileSearchFailure) +} + +public enum FileSearchFailure: Hashable, Sendable { + /// ripgrep is not installed where the search runs. + case ripgrepNotFound + /// ripgrep rejected the regular expression. Carries its diagnostic. + case invalidRegex(String?) + /// The scope cannot be searched, with a user-facing reason. + case unavailable(String) + /// The command failed. `message` is its trimmed stderr, possibly empty. + case processFailed(status: Int32, message: String) +} + +/// Runs one search somewhere (this Mac, an SSH host, a Cloud VM). +/// +/// Implementations stream batches into `sink` as they are found and return +/// how the search ended. They stop promptly when their task is cancelled. +public protocol FileSearchBackend: Sendable { + func search( + query: FileSearchQuery, + rootPath: String, + matchLimit: Int, + sink: FileSearchBatchMailbox + ) async -> FileSearchCompletion +} + +/// Coalesces batches produced off the main thread until the UI drains them. +/// +/// Producers call ``send(_:)`` per decoded chunk. The consumer waits on +/// ``signals``, which buffers at most one wake-up, then takes everything that +/// arrived since the last drain in one merged batch. However fast ripgrep +/// prints, the UI does one update per drain. +public final class FileSearchBatchMailbox: @unchecked Sendable { + public struct Drain { + public let groups: [FileSearchFileMatches] + /// Set once the producer finished and everything before it drained. + public let completion: FileSearchCompletion? + } + + public let signals: AsyncStream + private let signalContinuation: AsyncStream.Continuation + private let lock = NSLock() + private var pending: [FileSearchFileMatches] = [] + private var completion: FileSearchCompletion? + + public init() { + let (stream, continuation) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(1)) + signals = stream + signalContinuation = continuation + } + + public func send(_ groups: [FileSearchFileMatches]) { + guard !groups.isEmpty else { return } + lock.lock() + let isFinished = completion != nil + if !isFinished { pending.appendMerging(groups) } + lock.unlock() + if !isFinished { signalContinuation.yield() } + } + + public func finish(_ result: FileSearchCompletion) { + lock.lock() + let firstFinish = completion == nil + if firstFinish { completion = result } + lock.unlock() + guard firstFinish else { return } + signalContinuation.yield() + signalContinuation.finish() + } + + public func drain() -> Drain { + lock.lock() + defer { lock.unlock() } + let groups = pending + pending.removeAll(keepingCapacity: true) + return Drain(groups: groups, completion: completion) + } +} + +extension FileSearchFailure { + /// Printed on stderr by remote wrappers when `rg` is not on PATH. + public static let missingRipgrepMarker = "cmux-file-search: rg not found" +} + +/// Runs a command that prints `rg --json` and decodes it as it streams. +/// Shared by the local and SSH backends. +public struct RipgrepStreamingSearch: Sendable { + public let command: FileSearchCommand + public let matchLimit: Int + + public init(command: FileSearchCommand, matchLimit: Int) { + self.command = command + self.matchLimit = matchLimit + } + + public func run(sink: FileSearchBatchMailbox) async -> FileSearchCompletion { + let command = self.command + let matchLimit = self.matchLimit + let process: FileSearchProcess + do { + process = try FileSearchProcess(command: command) + } catch FileSearchProcess.SpawnError.launchFailed(let code) where code == ENOENT { + return .failed(.ripgrepNotFound) + } catch FileSearchProcess.SpawnError.launchFailed(let code) { + return .failed(.processFailed(status: -1, message: String(cString: strerror(code)))) + } catch { + return .failed(.processFailed(status: -1, message: String(describing: error))) + } + + let decoder = RipgrepStreamDecoder(matchLimit: matchLimit) + return await withTaskCancellationHandler { + for await chunk in process.standardOutputChunks() { + if Task.isCancelled { break } + sink.send(decoder.consume(chunk)) + if decoder.isLimitReached { + process.terminate() + break + } + } + sink.send(decoder.finish()) + let exit = await process.waitForExit() + return FileSearchCompletion( + ripgrepExitStatus: exit.status, + standardError: exit.standardError, + matchCount: decoder.matchCount, + limitReached: decoder.isLimitReached, + matchLimit: matchLimit + ) + } onCancel: { + process.terminate() + } + } +} + +extension FileSearchCompletion { + /// Maps ripgrep's exit to a completion. Exit 1 means no match; exit 2 + /// after matches means some files were unreadable, which is not a failure + /// of the search as a whole. + public init( + ripgrepExitStatus status: Int32, + standardError: String, + matchCount: Int, + limitReached: Bool, + matchLimit: Int + ) { + if limitReached { + self = .limited(matchLimit) + return + } + if status == 0 || status == 1 { + self = .completed + return + } + let message = standardError.trimmingCharacters(in: .whitespacesAndNewlines) + if message.contains(FileSearchFailure.missingRipgrepMarker) || (status == 127 && message.isEmpty) { + self = .failed(.ripgrepNotFound) + return + } + if message.contains("regex parse error") || message.contains("error parsing regex") || + message.contains("PCRE2") && message.contains("error") { + self = .failed(.invalidRegex(message)) + return + } + if status == 2, matchCount > 0 { + self = .completed + return + } + self = .failed(.processFailed(status: status, message: message)) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchEngine.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchEngine.swift new file mode 100644 index 000000000000..3b8987effef7 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchEngine.swift @@ -0,0 +1,209 @@ +import Foundation + +/// One search to run: what to find, where, and on which backend. +public struct FileSearchRequest: @unchecked Sendable { + public var query: FileSearchQuery + public var rootPath: String + /// Identifies the backend's target (local, an SSH host, a Cloud VM). + /// Two requests are the same search only when this matches. + public var scopeIdentity: String + /// The file tree's revision; a new revision re-runs an identical query. + public var contentRevision: Int + public var backend: any FileSearchBackend + + public init( + query: FileSearchQuery, + rootPath: String, + scopeIdentity: String, + contentRevision: Int = 0, + backend: any FileSearchBackend + ) { + self.query = query + self.rootPath = rootPath + self.scopeIdentity = scopeIdentity + self.contentRevision = contentRevision + self.backend = backend + } + + /// Equality of everything but the backend object. + public func isSameSearch(as other: FileSearchRequest) -> Bool { + query == other.query && rootPath == other.rootPath && + scopeIdentity == other.scopeIdentity && contentRevision == other.contentRevision + } +} + +public enum FileSearchPhase: Hashable, Sendable { + case idle + case searching + case finished(FileSearchCompletion) +} + +/// What the results view must apply, in order. +public enum FileSearchEngineEvent: Equatable, Sendable { + /// The tree was emptied; reload everything. + case reset + /// Rows were added to the tree. + case changed(FileSearchTreeChange) + /// ``FileSearchEngine/phase`` changed. + case phase(FileSearchPhase) +} + +/// Owns the current search: debounces requests, cancels superseded ones, +/// and applies streamed batches to ``tree`` at most once per frame. +@MainActor +public final class FileSearchEngine { + public let tree: FileSearchResultTree + public private(set) var phase: FileSearchPhase = .idle + public private(set) var activeRequest: FileSearchRequest? + public var onEvent: ((FileSearchEngineEvent) -> Void)? + + public let matchLimit: Int + private let clock: any Clock + private let debounceInterval: Duration + private let frameInterval: Duration + private var generation = 0 + private var debounceTask: Task? + private var searchTask: Task? + private var drainTask: Task? + + /// - Parameters: + /// - clock: Times the debounce and frame pacing. Tests inject a manual clock. + /// - relativePath: Maps a match path to the path shown on its file row. + public init( + clock: any Clock = ContinuousClock(), + debounceInterval: Duration = .milliseconds(200), + frameInterval: Duration = .milliseconds(16), + matchLimit: Int = 100_000, + relativePath: @escaping (String) -> String + ) { + self.clock = clock + self.debounceInterval = debounceInterval + self.frameInterval = frameInterval + self.matchLimit = matchLimit + self.tree = FileSearchResultTree(relativePath: relativePath) + } + + public var isSearching: Bool { phase == .searching } + + /// Starts `request` after the debounce interval unless another request + /// arrives first. A request identical to the running one is ignored. + public func schedule(_ request: FileSearchRequest) { + debounceTask?.cancel() + debounceTask = nil + if isRunning(request) { return } + let clock = self.clock + let interval = debounceInterval + debounceTask = Task { [weak self] in + do { + try await Self.sleep(clock, for: interval) + } catch { + return + } + guard let self, !Task.isCancelled else { return } + self.debounceTask = nil + self.start(request) + } + } + + /// Starts `request` now, cancelling any running or pending search. A + /// request identical to the running one is ignored. + public func start(_ request: FileSearchRequest) { + debounceTask?.cancel() + debounceTask = nil + if isRunning(request) { return } + stopRunningSearch() + generation += 1 + let searchGeneration = generation + activeRequest = request + tree.removeAll() + onEvent?(.reset) + + guard !request.query.isEmpty, !request.rootPath.isEmpty else { + setPhase(.idle) + return + } + if request.query.regexSyntaxError != nil { + setPhase(.finished(.failed(.invalidRegex(nil)))) + return + } + setPhase(.searching) + + let mailbox = FileSearchBatchMailbox() + let backend = request.backend + let query = request.query + let rootPath = request.rootPath + let limit = matchLimit + searchTask = Task.detached(priority: .userInitiated) { + let completion = await backend.search(query: query, rootPath: rootPath, matchLimit: limit, sink: mailbox) + mailbox.finish(completion) + } + let clock = self.clock + let frame = frameInterval + drainTask = Task { [weak self] in + for await _ in mailbox.signals { + guard let self, self.generation == searchGeneration else { return } + let drained = mailbox.drain() + if !drained.groups.isEmpty { + let change = self.tree.apply(drained.groups) + self.onEvent?(.changed(change)) + } + if let completion = drained.completion { + self.finish(generation: searchGeneration, completion: completion) + return + } + do { + try await Self.sleep(clock, for: frame) + } catch { + return + } + } + } + } + + /// Stops any pending or running search. `clearResults` also empties the tree. + public func cancel(clearResults: Bool) { + debounceTask?.cancel() + debounceTask = nil + let wasSearching = isSearching + stopRunningSearch() + generation += 1 + activeRequest = nil + if clearResults { + tree.removeAll() + onEvent?(.reset) + setPhase(.idle) + } else if wasSearching { + setPhase(.idle) + } + } + + private func isRunning(_ request: FileSearchRequest) -> Bool { + guard isSearching, let activeRequest else { return false } + return activeRequest.isSameSearch(as: request) + } + + private func finish(generation searchGeneration: Int, completion: FileSearchCompletion) { + guard searchGeneration == generation else { return } + searchTask = nil + drainTask = nil + setPhase(.finished(completion)) + } + + private func stopRunningSearch() { + searchTask?.cancel() + searchTask = nil + drainTask?.cancel() + drainTask = nil + } + + private func setPhase(_ next: FileSearchPhase) { + guard phase != next else { return } + phase = next + onEvent?(.phase(next)) + } + + private nonisolated static func sleep(_ clock: C, for duration: Duration) async throws + where C.Duration == Duration { + try await clock.sleep(until: clock.now.advanced(by: duration), tolerance: nil) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchHistory.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchHistory.swift new file mode 100644 index 000000000000..49be5612d89c --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchHistory.swift @@ -0,0 +1,74 @@ +/// Past search patterns, oldest first, without duplicates. +public struct FileSearchHistory: Hashable, Sendable, Codable { + public static let defaultCapacity = 50 + + public private(set) var entries: [String] + public let capacity: Int + + public init(entries: [String] = [], capacity: Int = defaultCapacity) { + self.capacity = max(1, capacity) + self.entries = [] + for entry in entries { record(entry) } + } + + /// Adds `pattern` as the newest entry, moving an existing copy. + public mutating func record(_ pattern: String) { + guard !pattern.isEmpty else { return } + entries.removeAll { $0 == pattern } + entries.append(pattern) + if entries.count > capacity { + entries.removeFirst(entries.count - capacity) + } + } +} + +/// Up/Down navigation through a ``FileSearchHistory`` from a query field. +/// +/// The first step back remembers what the user had typed, and stepping +/// forward past the newest entry returns to it, as in VS Code. +public struct FileSearchHistoryCursor: Hashable, Sendable { + private var index: Int? + private var draft = "" + + public init() {} + + /// True while the field shows a history entry rather than typed text. + public var isBrowsing: Bool { index != nil } + + /// The entry before the current one, or `nil` at the oldest entry. + public mutating func previous(in history: FileSearchHistory, current: String) -> String? { + guard !history.entries.isEmpty else { return nil } + if let index { + guard index > 0 else { return nil } + self.index = index - 1 + } else { + draft = current + var start = history.entries.count - 1 + // Skip the newest entry when it is what the field already shows. + if history.entries[start] == current { + guard start > 0 else { return nil } + start -= 1 + } + index = start + } + return history.entries[index!] + } + + /// The entry after the current one, the saved draft past the newest, or + /// `nil` when not browsing. + public mutating func next(in history: FileSearchHistory) -> String? { + guard let index else { return nil } + if index + 1 < history.entries.count { + self.index = index + 1 + return history.entries[index + 1] + } + self.index = nil + return draft + } + + /// Typing ends browsing. + public mutating func reset() { + index = nil + draft = "" + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchMatch.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchMatch.swift new file mode 100644 index 000000000000..1143061f8f5f --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchMatch.swift @@ -0,0 +1,54 @@ +/// One match: one ripgrep submatch. A line with three hits yields three +/// matches that share `lineNumber` and differ in `column`. +public struct FileSearchMatch: Hashable, Sendable { + /// 1-based line number. + public let lineNumber: Int + /// 1-based column in UTF-16 code units of the full line, as text views + /// and editor `path:line:column` arguments count it. + public let column: Int + /// Length of the match in UTF-16 code units of the full line. + public let length: Int + /// A bounded excerpt of the line around the match. Leading indentation is + /// dropped and a long prefix is replaced with an ellipsis. + public let preview: String + /// The match inside `preview`, as UTF-16 offsets. + public let previewMatchRange: Range + + public init(lineNumber: Int, column: Int, length: Int, preview: String, previewMatchRange: Range) { + self.lineNumber = lineNumber + self.column = column + self.length = length + self.preview = preview + self.previewMatchRange = previewMatchRange + } +} + +/// Matches found in one file, in ripgrep's output order. +public struct FileSearchFileMatches: Hashable, Sendable { + public let path: String + public var matches: [FileSearchMatch] + + public init(path: String, matches: [FileSearchMatch]) { + self.path = path + self.matches = matches + } +} + +extension Array where Element == FileSearchFileMatches { + /// Appends `other`, merging a leading group into the trailing one when + /// both name the same file so batches stay grouped across chunk borders. + public mutating func appendMerging(_ other: [FileSearchFileMatches]) { + for group in other { + if let lastIndex = indices.last, self[lastIndex].path == group.path { + self[lastIndex].matches += group.matches + } else { + append(group) + } + } + } + + /// Total matches across every file. + public var matchCount: Int { + reduce(0) { $0 + $1.matches.count } + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchProcess.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchProcess.swift new file mode 100644 index 000000000000..a05f52243fab --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchProcess.swift @@ -0,0 +1,225 @@ +public import Darwin +public import Foundation + +/// A command that prints `rg --json` output on stdout: ripgrep itself, or ssh +/// running ripgrep on another host. +public struct FileSearchCommand: Hashable, Sendable { + public var executablePath: String + public var arguments: [String] + /// Written to stdin and then closed. `nil` connects stdin to /dev/null. + public var standardInput: Data? + + public init(executablePath: String, arguments: [String], standardInput: Data? = nil) { + self.executablePath = executablePath + self.arguments = arguments + self.standardInput = standardInput + } +} + +/// A running child process with streamed stdout. +/// +/// Spawned with `posix_spawn` rather than `Process` for two reasons: +/// arguments reach the child byte for byte (Foundation converts them with +/// `fileSystemRepresentation`, which decomposes non-ASCII search text to NFD +/// and then misses NFC file contents), and the child leads its own process +/// group so cancellation also stops the processes ssh starts. +public final class FileSearchProcess: @unchecked Sendable { + public enum SpawnError: Error, Equatable { + case launchFailed(errno: Int32) + } + + public let pid: pid_t + private let stdoutRead: Int32 + private let stderrRead: Int32 + private let lock = NSLock() + private var exited = false + private var terminationRequested = false + + private static let ioQueue = DispatchQueue( + label: "com.cmux.file-search.process-io", + qos: .userInitiated, + attributes: .concurrent + ) + + /// Starts `command`. Throws when the executable cannot be launched. + public init(command: FileSearchCommand) throws { + var stdoutPipe: [Int32] = [-1, -1] + var stderrPipe: [Int32] = [-1, -1] + var stdinPipe: [Int32] = [-1, -1] + guard pipe(&stdoutPipe) == 0 else { throw SpawnError.launchFailed(errno: errno) } + guard pipe(&stderrPipe) == 0 else { + let code = errno + Self.close(stdoutPipe) + throw SpawnError.launchFailed(errno: code) + } + if command.standardInput != nil { + guard pipe(&stdinPipe) == 0 else { + let code = errno + Self.close(stdoutPipe + stderrPipe) + throw SpawnError.launchFailed(errno: code) + } + } + + var fileActions: posix_spawn_file_actions_t? + posix_spawn_file_actions_init(&fileActions) + defer { posix_spawn_file_actions_destroy(&fileActions) } + if command.standardInput != nil { + posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], STDIN_FILENO) + } else { + posix_spawn_file_actions_addopen(&fileActions, STDIN_FILENO, "/dev/null", O_RDONLY, 0) + } + posix_spawn_file_actions_adddup2(&fileActions, stdoutPipe[1], STDOUT_FILENO) + posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], STDERR_FILENO) + + var attributes: posix_spawnattr_t? + posix_spawnattr_init(&attributes) + defer { posix_spawnattr_destroy(&attributes) } + // Only the three standard descriptors survive into the child, and the + // child starts a new process group we can signal as a whole. + // Ignored signal dispositions and the signal mask survive exec, so a + // host that ignores SIGTERM or SIGPIPE would otherwise make the child + // unkillable or change how ripgrep handles a closed pipe. + posix_spawnattr_setflags( + &attributes, + Int16(POSIX_SPAWN_CLOEXEC_DEFAULT | POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK) + ) + posix_spawnattr_setpgroup(&attributes, 0) + var defaultSignals = sigset_t() + sigfillset(&defaultSignals) + sigdelset(&defaultSignals, SIGKILL) + sigdelset(&defaultSignals, SIGSTOP) + posix_spawnattr_setsigdefault(&attributes, &defaultSignals) + var emptyMask = sigset_t() + sigemptyset(&emptyMask) + posix_spawnattr_setsigmask(&attributes, &emptyMask) + + let argv = [command.executablePath] + command.arguments + var cArguments: [UnsafeMutablePointer?] = argv.map { strdup($0) } + [nil] + defer { for pointer in cArguments { free(pointer) } } + + var childPID: pid_t = 0 + let status = posix_spawn(&childPID, command.executablePath, &fileActions, &attributes, &cArguments, environ) + Self.close([stdoutPipe[1], stderrPipe[1]]) + if command.standardInput != nil { Self.close([stdinPipe[0]]) } + guard status == 0 else { + Self.close([stdoutPipe[0], stderrPipe[0]]) + if command.standardInput != nil { Self.close([stdinPipe[1]]) } + throw SpawnError.launchFailed(errno: status) + } + pid = childPID + stdoutRead = stdoutPipe[0] + stderrRead = stderrPipe[0] + + if let input = command.standardInput { + let writeEnd = stdinPipe[1] + // A remote shell may exit before reading everything; never die of SIGPIPE. + _ = fcntl(writeEnd, F_SETNOSIGPIPE, 1) + Self.ioQueue.async { + input.withUnsafeBytes { buffer in + var offset = 0 + while offset < buffer.count { + let written = Darwin.write(writeEnd, buffer.baseAddress! + offset, buffer.count - offset) + if written > 0 { + offset += written + } else if written < 0, errno == EINTR { + continue + } else { + break + } + } + } + Darwin.close(writeEnd) + } + } + } + + /// Stdout chunks until end of file. The stream finishing does not mean + /// the process exited; call ``waitForExit()`` for the status. + public func standardOutputChunks(chunkSize: Int = 64 * 1024) -> AsyncStream { + let descriptor = stdoutRead + return AsyncStream(bufferingPolicy: .unbounded) { continuation in + continuation.onTermination = { [weak self] termination in + if case .cancelled = termination { self?.terminate() } + } + Self.ioQueue.async { + var buffer = [UInt8](repeating: 0, count: chunkSize) + while true { + let count = buffer.withUnsafeMutableBytes { Darwin.read(descriptor, $0.baseAddress, $0.count) } + if count > 0 { + continuation.yield(Data(buffer[0.. (status: Int32, standardError: String) { + let descriptor = stderrRead + let pid = self.pid + return await withCheckedContinuation { continuation in + Self.ioQueue.async { + var collected = Data() + var buffer = [UInt8](repeating: 0, count: 8 * 1024) + while true { + let count = buffer.withUnsafeMutableBytes { Darwin.read(descriptor, $0.baseAddress, $0.count) } + if count > 0 { + collected.append(contentsOf: buffer[0.. 8 * 1024 { + collected.removeFirst(collected.count - 8 * 1024) + } + } else if count < 0, errno == EINTR { + continue + } else { + break + } + } + Darwin.close(descriptor) + // Observe the exit without reaping so the pid (and its group + // id) cannot be reused before `terminate` learns it is gone. + var info = siginfo_t() + while waitid(P_PID, id_t(pid), &info, WEXITED | WNOWAIT) < 0, errno == EINTR {} + self.markExited() + var rawStatus: Int32 = 0 + while waitpid(pid, &rawStatus, 0) < 0, errno == EINTR {} + continuation.resume(returning: ( + Self.exitCode(fromWaitStatus: rawStatus), + String(decoding: collected, as: UTF8.self) + )) + } + } + } + + /// Sends SIGTERM to the child's process group. Safe to call repeatedly + /// and after exit; a reaped pid is never signalled. + public func terminate() { + lock.lock() + defer { lock.unlock() } + guard !exited, !terminationRequested else { return } + terminationRequested = true + _ = Darwin.kill(-pid, SIGTERM) + } + + private func markExited() { + lock.lock() + exited = true + lock.unlock() + } + + /// Shell-style exit code: the status, or 128 + signal number. + static func exitCode(fromWaitStatus status: Int32) -> Int32 { + let signal = status & 0x7F + if signal == 0 { return (status >> 8) & 0xFF } + return 128 + signal + } + + private static func close(_ descriptors: [Int32]) { + for descriptor in descriptors where descriptor >= 0 { Darwin.close(descriptor) } + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchQuery.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchQuery.swift new file mode 100644 index 000000000000..ba2ee73f2c17 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchQuery.swift @@ -0,0 +1,66 @@ +import Foundation + +/// Everything the user controls in a project-wide search: the pattern and the +/// VS Code style toggles and glob fields. +public struct FileSearchQuery: Hashable, Sendable, Codable { + /// The text or regular expression to find. + public var pattern: String + /// Match Case. When off, the search is case-insensitive. + public var isCaseSensitive: Bool + /// Match Whole Word. + public var matchesWholeWord: Bool + /// Use Regular Expression. When off, the pattern is a literal string. + public var isRegex: Bool + /// Comma-separated globs a file must match ("files to include"). + public var includePatterns: String + /// Comma-separated globs that remove files ("files to exclude"). + public var excludePatterns: String + /// Use Exclude Settings and Ignore Files: honor `.gitignore`/`.ignore` + /// and skip the built-in generated directories. + public var usesIgnoreFiles: Bool + + public init( + pattern: String = "", + isCaseSensitive: Bool = false, + matchesWholeWord: Bool = false, + isRegex: Bool = false, + includePatterns: String = "", + excludePatterns: String = "", + usesIgnoreFiles: Bool = true + ) { + self.pattern = pattern + self.isCaseSensitive = isCaseSensitive + self.matchesWholeWord = matchesWholeWord + self.isRegex = isRegex + self.includePatterns = includePatterns + self.excludePatterns = excludePatterns + self.usesIgnoreFiles = usesIgnoreFiles + } + + /// True when there is nothing to search for. Whitespace is a valid + /// literal pattern, so only the empty string counts. + public var isEmpty: Bool { pattern.isEmpty } + + /// A syntax problem in a regular-expression pattern, found before ripgrep + /// runs so the field can show it inline while the user types. ripgrep's own + /// parser remains authoritative; its error is reported as well. + public var regexSyntaxError: FileSearchRegexSyntaxError? { + guard isRegex, !pattern.isEmpty else { return nil } + do { + _ = try NSRegularExpression(pattern: pattern) + return nil + } catch { + return FileSearchRegexSyntaxError(detail: nil) + } + } +} + +/// An invalid regular expression. `detail` carries ripgrep's diagnostic +/// when it rejected the pattern; the local precheck has none. +public struct FileSearchRegexSyntaxError: Hashable, Sendable { + public let detail: String? + + public init(detail: String?) { + self.detail = detail + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchResultTree.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchResultTree.swift new file mode 100644 index 000000000000..3c8fef57a8c5 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchResultTree.swift @@ -0,0 +1,180 @@ +/// A file row in the results outline. Reference identity is the outline +/// item identity, so a file keeps its row, selection and expansion while +/// matches stream in. +public final class FileSearchFileNode { + public let path: String + public let relativePath: String + public private(set) var matches: [FileSearchMatch] + /// The user's collapse choice. New files start expanded. + public var isExpanded = true + private var matchNodes: [FileSearchMatchNode?] + + init(path: String, relativePath: String, matches: [FileSearchMatch]) { + self.path = path + self.relativePath = relativePath + self.matches = matches + self.matchNodes = Array(repeating: nil, count: matches.count) + } + + /// The row object for one match, created on first request so a collapsed + /// file with many matches allocates nothing per match. + public func matchNode(at index: Int) -> FileSearchMatchNode { + if let node = matchNodes[index] { return node } + let node = FileSearchMatchNode(file: self, index: index) + matchNodes[index] = node + return node + } + + func append(_ newMatches: [FileSearchMatch]) { + matches += newMatches + matchNodes += Array(repeating: nil, count: newMatches.count) + } +} + +/// A match row. `file` is unowned-safe because the tree owns every file node +/// for as long as the outline can ask for its children. +public final class FileSearchMatchNode { + public unowned let file: FileSearchFileNode + public let index: Int + + init(file: FileSearchFileNode, index: Int) { + self.file = file + self.index = index + } + + public var match: FileSearchMatch { file.matches[index] } +} + +/// What one batch changed, in the order an outline must apply it: first the +/// grown existing files, then the appended files. +public struct FileSearchTreeChange: Equatable, Sendable { + /// Existing files that gained matches, with their previous match counts. + public var grownFiles: [(fileIndex: Int, previousCount: Int)] + /// Newly appended files. + public var insertedFiles: Range + + public var isEmpty: Bool { grownFiles.isEmpty && insertedFiles.isEmpty } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.insertedFiles == rhs.insertedFiles && + lhs.grownFiles.map(\.fileIndex) == rhs.grownFiles.map(\.fileIndex) && + lhs.grownFiles.map(\.previousCount) == rhs.grownFiles.map(\.previousCount) + } +} + +/// A position in results order, used by next/previous navigation. +public struct FileSearchResultPosition: Hashable, Sendable { + public let fileIndex: Int + /// `nil` for the file row itself. + public let matchIndex: Int? + + public init(fileIndex: Int, matchIndex: Int?) { + self.fileIndex = fileIndex + self.matchIndex = matchIndex + } +} + +/// Search results grouped by file, in first-seen order. +public final class FileSearchResultTree { + public private(set) var files: [FileSearchFileNode] = [] + public private(set) var matchCount = 0 + private var fileIndexByPath: [String: Int] = [:] + private let relativePath: (String) -> String + + /// `relativePath` maps an absolute match path to the path shown in the row. + public init(relativePath: @escaping (String) -> String) { + self.relativePath = relativePath + } + + public var fileCount: Int { files.count } + public var isEmpty: Bool { files.isEmpty } + + public func removeAll() { + files.removeAll() + fileIndexByPath.removeAll() + matchCount = 0 + } + + /// Merges a batch. Matches for a file already listed join that file even + /// when ripgrep reports it again later. + @discardableResult + public func apply(_ batch: [FileSearchFileMatches]) -> FileSearchTreeChange { + let firstNewIndex = files.count + var grown: [Int: Int] = [:] + for group in batch where !group.matches.isEmpty { + matchCount += group.matches.count + if let index = fileIndexByPath[group.path] { + if index < firstNewIndex, grown[index] == nil { + grown[index] = files[index].matches.count + } + files[index].append(group.matches) + } else { + fileIndexByPath[group.path] = files.count + files.append(FileSearchFileNode( + path: group.path, + relativePath: relativePath(group.path), + matches: group.matches + )) + } + } + return FileSearchTreeChange( + grownFiles: grown.sorted { $0.key < $1.key }.map { (fileIndex: $0.key, previousCount: $0.value) }, + insertedFiles: firstNewIndex.. Bool { + guard let index = index(of: file) else { return false } + files.remove(at: index) + matchCount -= file.matches.count + fileIndexByPath.removeValue(forKey: file.path) + for later in index.. Int? { + guard let index = fileIndexByPath[file.path], files[index] === file else { return nil } + return index + } + + public func position(of node: FileSearchMatchNode) -> FileSearchResultPosition? { + guard let fileIndex = index(of: node.file) else { return nil } + return FileSearchResultPosition(fileIndex: fileIndex, matchIndex: node.index) + } + + /// The first match after `position` in results order, wrapping around. + /// From a file row, that file's first match is next. + public func nextMatch(after position: FileSearchResultPosition?) -> FileSearchResultPosition? { + guard matchCount > 0 else { return nil } + var fileIndex = position?.fileIndex ?? 0 + var matchIndex = position.map { ($0.matchIndex ?? -1) + 1 } ?? 0 + for _ in 0...files.count { + if fileIndex < files.count, matchIndex < files[fileIndex].matches.count { + return FileSearchResultPosition(fileIndex: fileIndex, matchIndex: matchIndex) + } + fileIndex = (fileIndex + 1) % files.count + matchIndex = 0 + } + return nil + } + + /// The last match before `position` in results order, wrapping around. + /// From a file row, the previous file's last match is previous. + public func previousMatch(before position: FileSearchResultPosition?) -> FileSearchResultPosition? { + guard matchCount > 0 else { return nil } + var fileIndex = position?.fileIndex ?? 0 + var matchIndex = (position?.matchIndex ?? 0) - 1 + for _ in 0...files.count { + if fileIndex < files.count, matchIndex >= 0, matchIndex < files[fileIndex].matches.count { + return FileSearchResultPosition(fileIndex: fileIndex, matchIndex: matchIndex) + } + fileIndex = (fileIndex - 1 + files.count) % files.count + matchIndex = files[fileIndex].matches.count - 1 + } + return nil + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchRowList.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchRowList.swift new file mode 100644 index 000000000000..f00ebe45ec5e --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/FileSearchRowList.swift @@ -0,0 +1,96 @@ +public import Foundation + +/// The results as the flat list of rows a table shows: each file row +/// followed by its match rows unless the file is collapsed. +/// +/// A flat table replaces an outline because `NSOutlineView` expansion is +/// linear in the rows below the item, so expanding each streamed file costs +/// seconds at 100k matches. Appending rows to a table is constant time per +/// row; collapsing rebuilds the list, which is linear once per user action. +public final class FileSearchRowList { + public private(set) var rows: [FileSearchResultPosition] = [] + private let tree: FileSearchResultTree + + public init(tree: FileSearchResultTree) { + self.tree = tree + } + + public var count: Int { rows.count } + + public enum Update: Equatable { + /// Rows were appended at the end; `refreshedRows` are file rows whose + /// match count changed. + case appended(inserted: Range, refreshedRows: IndexSet) + /// Rows moved; reload everything. + case reload + } + + /// Rebuilds every row from the tree. + public func rebuild() { + var next: [FileSearchResultPosition] = [] + next.reserveCapacity(tree.fileCount + tree.matchCount) + for (fileIndex, file) in tree.files.enumerated() { + next.append(FileSearchResultPosition(fileIndex: fileIndex, matchIndex: nil)) + guard file.isExpanded else { continue } + for matchIndex in file.matches.indices { + next.append(FileSearchResultPosition(fileIndex: fileIndex, matchIndex: matchIndex)) + } + } + rows = next + } + + /// Applies a tree change. Streamed output appends at the end, except + /// when a file other than the last one gains matches, which reloads. + public func apply(_ change: FileSearchTreeChange) -> Update { + var refreshed = IndexSet() + let start = rows.count + let lastFileIndex = rows.last?.fileIndex + for grown in change.grownFiles { + guard grown.fileIndex == lastFileIndex else { + rebuild() + return .reload + } + let file = tree.files[grown.fileIndex] + if let fileRow = rows.lastIndex(where: { $0.fileIndex == grown.fileIndex && $0.matchIndex == nil }) { + refreshed.insert(fileRow) + } + guard file.isExpanded else { continue } + for matchIndex in grown.previousCount.. Int? { + var low = 0 + var high = rows.count + while low < high { + let mid = (low + high) / 2 + if Self.precedes(rows[mid], position) { low = mid + 1 } else { high = mid } + } + guard low < rows.count, rows[low] == position else { return nil } + return low + } + + /// The file row at or above `row`. + public func fileRow(containing row: Int) -> Int? { + guard row >= 0, row < rows.count else { return nil } + return self.row(of: FileSearchResultPosition(fileIndex: rows[row].fileIndex, matchIndex: nil)) + } + + private static func precedes(_ lhs: FileSearchResultPosition, _ rhs: FileSearchResultPosition) -> Bool { + if lhs.fileIndex != rhs.fileIndex { return lhs.fileIndex < rhs.fileIndex } + return (lhs.matchIndex ?? -1) < (rhs.matchIndex ?? -1) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepArguments.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepArguments.swift new file mode 100644 index 000000000000..a995726fea73 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepArguments.swift @@ -0,0 +1,111 @@ +import Foundation + +/// Maps a query to ripgrep's command line. +/// +/// The same argument vector runs locally, over SSH, and on a Cloud VM, so the +/// three scopes can never disagree about what a toggle means. +extension FileSearchQuery { + /// Generated directories skipped while "Use Exclude Settings and Ignore + /// Files" is on. `.git` is always skipped because `--hidden` would + /// otherwise search object storage. + public static let defaultExcludedDirectories = ["node_modules", "dist", "build", "DerivedData"] + + /// The full argument vector after the `rg` executable name. + public func ripgrepArguments(rootPath: String) -> [String] { + let query = self + var arguments = [ + "--json", + "--no-config", + "--no-messages", + "--hidden", + query.isCaseSensitive ? "--case-sensitive" : "--ignore-case", + ] + if query.isRegex { + // Falls back to PCRE2 for look-around and backreferences when the + // installed ripgrep has it. Accepted by ripgrep 11 and later. + arguments.append("--auto-hybrid-regex") + } else { + arguments.append("--fixed-strings") + } + if query.matchesWholeWord { + arguments.append("--word-regexp") + } + if !query.usesIgnoreFiles { + arguments.append("--no-ignore") + } + arguments += ["--glob", "!.git"] + if query.usesIgnoreFiles { + for directory in Self.defaultExcludedDirectories { + arguments += ["--glob", "!\(directory)"] + } + } + for glob in FileSearchGlobList(query.includePatterns).ripgrepGlobs { + arguments += ["--glob", glob] + } + for glob in FileSearchGlobList(query.excludePatterns).ripgrepGlobs { + arguments += ["--glob", "!\(glob)"] + } + arguments += ["--", query.pattern, rootPath] + return arguments + } +} + +/// One of VS Code's comma-separated glob fields, translated to ripgrep globs. +public struct FileSearchGlobList: Hashable, Sendable { + /// The trimmed, non-empty entries of the field. + public let entries: [String] + + public init(_ text: String) { + entries = Self.split(text) + } + + /// Splits on commas outside `{...}` alternations and trims each entry. + private static func split(_ text: String) -> [String] { + var entries: [String] = [] + var current = "" + var braceDepth = 0 + for character in text { + switch character { + case "{": + braceDepth += 1 + current.append(character) + case "}": + braceDepth = max(0, braceDepth - 1) + current.append(character) + case "," where braceDepth == 0: + entries.append(current) + current = "" + default: + current.append(character) + } + } + entries.append(current) + return entries + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + + /// Each entry becomes the glob itself plus, when its last component is a + /// plain name, a variant that also matches everything inside a folder of + /// that name. A name without a slash matches at any depth, like VS Code's + /// `src` meaning `**/src/**`; a name with a slash stays anchored to the root. + public var ripgrepGlobs: [String] { + var globs: [String] = [] + for rawEntry in entries { + var entry = rawEntry + if entry.hasPrefix("!") { entry.removeFirst() } + while entry.hasPrefix("./") { entry.removeFirst(2) } + while entry.count > 1, entry.hasSuffix("/") { entry.removeLast() } + guard !entry.isEmpty, entry != "." else { continue } + globs.append(entry) + let lastComponent = entry.split(separator: "/").last.map(String.init) ?? entry + guard !lastComponent.contains(where: { "*?[{".contains($0) }) else { continue } + if entry.contains("/") { + globs.append("\(entry)/**") + } else { + globs.append("**/\(entry)/**") + } + } + return globs + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepJSONLineParser.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepJSONLineParser.swift new file mode 100644 index 000000000000..a19402960c89 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepJSONLineParser.swift @@ -0,0 +1,218 @@ +import Foundation + +/// Parses one line of `rg --json` output. +/// +/// ripgrep reports submatch offsets in UTF-8 bytes of the line. Paths and +/// lines arrive as `{"text": ...}` or, when they are not valid UTF-8, as +/// `{"bytes": base64}`. Byte offsets are converted to UTF-16 so AppKit text +/// ranges and editor columns line up with what the user sees. +public struct RipgrepJSONLineParser: Sendable { + /// UTF-16 units of context kept before a match in a preview. + public let previewLeadingContext: Int + /// Upper bound on a stored preview, in UTF-16 units. + public let previewMaximumLength: Int + + public init(previewLeadingContext: Int = 40, previewMaximumLength: Int = 250) { + self.previewLeadingContext = previewLeadingContext + self.previewMaximumLength = previewMaximumLength + } + + private static let matchPrefix = Array(#"{"type":"match""#.utf8) + + /// The matches on one `match` line, or `nil` for any other event or a + /// line that does not parse. + public func parseMatch>(line bytes: Bytes) -> FileSearchFileMatches? { + guard bytes.starts(with: Self.matchPrefix) else { return nil } + let data = Data(bytes) + guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let payload = object["data"] as? [String: Any], + let pathObject = payload["path"] as? [String: Any], + let path = Self.decodedString(from: pathObject), + let linesObject = payload["lines"] as? [String: Any], + let lineNumber = payload["line_number"] as? Int else { + return nil + } + let byteRanges = (payload["submatches"] as? [[String: Any]] ?? []).compactMap { raw -> Range? in + guard let start = raw["start"] as? Int, let end = raw["end"] as? Int, + start >= 0, end >= start else { return nil } + return start..]) -> [FileSearchMatch] { + var units = Array(text.utf16) + if units.last == 0x0A { units.removeLast() } + if units.last == 0x0D { units.removeLast() } + let ranges = Self.sortedRanges(byteRanges) + let boundaries = Self.sortedBoundaries(ranges) + var unitOffsets = [Int](repeating: units.count, count: boundaries.count) + var boundaryIndex = 0 + var byteOffset = 0 + var unitIndex = 0 + while boundaryIndex < boundaries.count, boundaries[boundaryIndex] <= 0 { + unitOffsets[boundaryIndex] = 0 + boundaryIndex += 1 + } + while unitIndex < units.count, boundaryIndex < boundaries.count { + let unit = units[unitIndex] + if unit < 0x80 { + byteOffset += 1 + unitIndex += 1 + } else if unit < 0x800 { + byteOffset += 2 + unitIndex += 1 + } else if UTF16.isLeadSurrogate(unit), unitIndex + 1 < units.count, + UTF16.isTrailSurrogate(units[unitIndex + 1]) { + byteOffset += 4 + unitIndex += 2 + } else { + byteOffset += 3 + unitIndex += 1 + } + while boundaryIndex < boundaries.count, boundaries[boundaryIndex] <= byteOffset { + unitOffsets[boundaryIndex] = unitIndex + boundaryIndex += 1 + } + } + return makeMatches(units: &units, lineNumber: lineNumber, ranges: ranges, boundaries: boundaries, unitOffsets: unitOffsets) + } + + /// Converts byte-offset submatches on a line that is not valid UTF-8. + /// Each gap between boundaries decodes on its own, so every invalid + /// sequence becomes one U+FFFD in both the offsets and the rendered line. + func makeMatches(lineBytes rawLineBytes: [UInt8], lineNumber: Int, byteRanges: [Range]) -> [FileSearchMatch] { + var lineBytes = rawLineBytes + if lineBytes.last == 0x0A { lineBytes.removeLast() } + if lineBytes.last == 0x0D { lineBytes.removeLast() } + let byteCount = lineBytes.count + let ranges = Self.sortedRanges(byteRanges) + let boundaries = Self.sortedBoundaries(ranges).map { min($0, byteCount) } + var unitOffsets: [Int] = [] + var units: [UInt16] = [] + units.reserveCapacity(byteCount) + var cursor = 0 + for boundary in boundaries { + if boundary > cursor { + units.append(contentsOf: String(decoding: lineBytes[cursor..]) -> [Range] { + ranges.isEmpty ? [0..<0] : ranges.sorted { $0.lowerBound < $1.lowerBound } + } + + private static func sortedBoundaries(_ ranges: [Range]) -> [Int] { + var boundaries: [Int] = [] + boundaries.reserveCapacity(ranges.count * 2) + for range in ranges { + boundaries.append(range.lowerBound) + boundaries.append(range.upperBound) + } + boundaries.sort() + return boundaries + } + + private func makeMatches( + units: inout [UInt16], + lineNumber: Int, + ranges: [Range], + boundaries: [Int], + unitOffsets: [Int] + ) -> [FileSearchMatch] { + // Tabs render as one space so previews stay on one visual line + // without shifting the offsets. + for index in units.indices where units[index] == 0x09 { units[index] = 0x20 } + let firstContent = units.firstIndex { $0 != 0x20 } ?? units.count + func unitOffset(forByte byte: Int) -> Int { + // Binary search over the sorted boundaries of this line. + var low = 0 + var high = boundaries.count - 1 + while low < high { + let mid = (low + high) / 2 + if boundaries[mid] < byte { low = mid + 1 } else { high = mid } + } + return unitOffsets[low] + } + return ranges.map { range in + let start = unitOffset(forByte: range.lowerBound) + let end = max(start, unitOffset(forByte: range.upperBound)) + let preview = makePreview(units: units, firstContent: firstContent, matchStart: start, matchEnd: end) + return FileSearchMatch( + lineNumber: lineNumber, + column: start + 1, + length: end - start, + preview: preview.text, + previewMatchRange: preview.matchRange + ) + } + } + + private func makePreview( + units: [UInt16], + firstContent: Int, + matchStart: Int, + matchEnd: Int + ) -> (text: String, matchRange: Range) { + var windowStart: Int + let elided: Bool + if matchStart - firstContent <= previewLeadingContext { + windowStart = min(firstContent, matchStart) + elided = false + } else { + windowStart = matchStart - previewLeadingContext + elided = true + } + windowStart = Self.adjustedToScalarBoundary(windowStart, in: units) + // Keep a long match visible up to twice the preview budget; the + // stored string stays bounded however long the line or match is. + let budgetEnd = windowStart + previewMaximumLength + var windowEnd = min(units.count, max(budgetEnd, min(matchEnd, budgetEnd + previewMaximumLength))) + windowEnd = Self.adjustedToScalarBoundary(windowEnd, in: units) + while windowEnd > max(windowStart, matchEnd), units[windowEnd - 1] == 0x20 { windowEnd -= 1 } + + let prefix = elided ? "\u{2026}" : "" + let body = String(decoding: units[windowStart.. Int { + guard offset > 0, offset < units.count else { return offset } + return UTF16.isTrailSurrogate(units[offset]) ? offset - 1 : offset + } + + private static func decodedString(from object: [String: Any]) -> String? { + if let text = object["text"] as? String { + return text + } + guard let encoded = object["bytes"] as? String, + let data = Data(base64Encoded: encoded) else { + return nil + } + return String(decoding: data, as: UTF8.self) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepStreamDecoder.swift b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepStreamDecoder.swift new file mode 100644 index 000000000000..9776e33d2298 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Sources/CmuxFileSearch/RipgrepStreamDecoder.swift @@ -0,0 +1,72 @@ +import Foundation + +/// Turns ripgrep's stdout, in arbitrary chunks, into grouped matches. +/// +/// Owned by one background task at a time. It keeps only the unfinished +/// trailing line between calls, so memory tracks the output rate rather than +/// the total output size. +public final class RipgrepStreamDecoder { + /// Stop after this many matches. + public let matchLimit: Int + public private(set) var matchCount = 0 + /// True once `matchLimit` matches were decoded; later input is ignored. + public private(set) var isLimitReached = false + private var pending: [UInt8] = [] + private let parser: RipgrepJSONLineParser + + public init(matchLimit: Int, parser: RipgrepJSONLineParser = RipgrepJSONLineParser()) { + self.matchLimit = max(1, matchLimit) + self.parser = parser + } + + /// Decodes every complete line in `chunk`. + public func consume>(_ chunk: Bytes) -> [FileSearchFileMatches] { + guard !isLimitReached else { return [] } + pending.append(contentsOf: chunk) + var output: [FileSearchFileMatches] = [] + var lineStart = 0 + pending.withUnsafeBufferPointer { buffer in + guard let base = buffer.baseAddress else { return } + while lineStart < buffer.count, !isLimitReached { + let remaining = buffer.count - lineStart + guard let newline = memchr(base + lineStart, 0x0A, remaining) else { break } + let lineEnd = base.distance(to: newline.assumingMemoryBound(to: UInt8.self)) + decodeLine(UnsafeBufferPointer(rebasing: buffer[lineStart.. 0 { + pending.removeFirst(lineStart) + } + return output + } + + /// Decodes an unterminated final line, if any. + public func finish() -> [FileSearchFileMatches] { + guard !isLimitReached, !pending.isEmpty else { + pending.removeAll() + return [] + } + var output: [FileSearchFileMatches] = [] + let line = pending + pending.removeAll() + line.withUnsafeBufferPointer { decodeLine($0, into: &output) } + return output + } + + private func decodeLine(_ line: UnsafeBufferPointer, into output: inout [FileSearchFileMatches]) { + guard var group = parser.parseMatch(line: line) else { return } + // "Limited" means a match was dropped, so a search with exactly + // `matchLimit` results still completes normally. + let room = matchLimit - matchCount + if group.matches.count > room { + group.matches = Array(group.matches.prefix(room)) + isLimitReached = true + } + guard !group.matches.isEmpty else { return } + matchCount += group.matches.count + output.appendMerging([group]) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchEngineTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchEngineTests.swift new file mode 100644 index 000000000000..8e01d3e7a64b --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchEngineTests.swift @@ -0,0 +1,263 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +/// A backend the test drives by hand: each search waits until the test +/// completes it, and cancellation is recorded. +final class ScriptedBackend: FileSearchBackend, @unchecked Sendable { + final class Run: @unchecked Sendable { + let query: FileSearchQuery + let sink: FileSearchBatchMailbox + var continuation: CheckedContinuation? + var wasCancelled = false + + init(query: FileSearchQuery, sink: FileSearchBatchMailbox) { + self.query = query + self.sink = sink + } + } + + private let lock = NSLock() + private var runs: [Run] = [] + private let started = AsyncStream.makeStream() + + func search(query: FileSearchQuery, rootPath: String, matchLimit: Int, sink: FileSearchBatchMailbox) async -> FileSearchCompletion { + let run = Run(query: query, sink: sink) + return await withTaskCancellationHandler { + await withCheckedContinuation { continuation in + lock.lock() + run.continuation = continuation + runs.append(run) + let count = runs.count + let cancelled = run.wasCancelled + lock.unlock() + started.continuation.yield(count) + if cancelled { resume(run, with: .completed) } + } + } onCancel: { + lock.lock() + run.wasCancelled = true + lock.unlock() + resume(run, with: .completed) + } + } + + private func resume(_ run: Run, with completion: FileSearchCompletion) { + lock.lock() + let continuation = run.continuation + run.continuation = nil + lock.unlock() + continuation?.resume(returning: completion) + } + + func run(_ index: Int) -> Run { + lock.lock() + defer { lock.unlock() } + return runs[index] + } + + var runCount: Int { + lock.lock() + defer { lock.unlock() } + return runs.count + } + + func complete(_ index: Int, with completion: FileSearchCompletion) { + resume(run(index), with: completion) + } + + func waitForRuns(_ count: Int) async { + if runCount >= count { return } + for await value in started.stream where value >= count { return } + } +} + +@MainActor +private final class EventRecorder { + var events: [FileSearchEngineEvent] = [] + private let stream = AsyncStream.makeStream() + + func attach(to engine: FileSearchEngine) { + engine.onEvent = { [weak self] event in + self?.events.append(event) + self?.stream.continuation.yield(event) + } + } + + /// Waits for the first event after the current point that satisfies `predicate`. + func wait(for predicate: (FileSearchEngineEvent) -> Bool) async { + for await event in stream.stream where predicate(event) { return } + } +} + +private func group(_ path: String, lines: [Int]) -> FileSearchFileMatches { + FileSearchFileMatches( + path: path, + matches: lines.map { FileSearchMatch(lineNumber: $0, column: 1, length: 1, preview: "x", previewMatchRange: 0..<1) } + ) +} + +@MainActor +@Suite("Search engine lifecycle", .serialized) +struct FileSearchEngineTests { + private func request(_ pattern: String, backend: ScriptedBackend, revision: Int = 0, isRegex: Bool = false) -> FileSearchRequest { + FileSearchRequest( + query: FileSearchQuery(pattern: pattern, isRegex: isRegex), + rootPath: "/root", + scopeIdentity: "local", + contentRevision: revision, + backend: backend + ) + } + + private func makeEngine(clock: ManualTestClock, frame: Duration = .zero) -> FileSearchEngine { + FileSearchEngine(clock: clock, debounceInterval: .milliseconds(200), frameInterval: frame, matchLimit: 1_000) { + String($0.dropFirst("/root/".count)) + } + } + + @Test("A burst of scheduled queries runs only the last one, after the debounce") + func debounce() async { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock) + + for pattern in ["p", "pr", "pri", "priv"] { + engine.schedule(request(pattern, backend: backend)) + } + await clock.waitForSleepers(1) + #expect(backend.runCount == 0) + + clock.advance(by: .milliseconds(199)) + #expect(backend.runCount == 0) + clock.advance(by: .milliseconds(1)) + await backend.waitForRuns(1) + + #expect(backend.runCount == 1) + #expect(backend.run(0).query.pattern == "priv") + engine.cancel(clearResults: true) + } + + @Test("A new query cancels the running one and its late output is discarded") + func supersededSearchIsCancelled() async { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock) + let recorder = EventRecorder() + recorder.attach(to: engine) + + engine.start(request("old", backend: backend)) + await backend.waitForRuns(1) + engine.start(request("new", backend: backend)) + await backend.waitForRuns(2) + + #expect(backend.run(0).wasCancelled) + backend.run(0).sink.send([group("/root/stale", lines: [1])]) + backend.run(1).sink.send([group("/root/fresh", lines: [1, 2])]) + backend.complete(1, with: .completed) + await recorder.wait { $0 == .phase(.finished(.completed)) } + + #expect(engine.tree.files.map(\.relativePath) == ["fresh"]) + #expect(engine.tree.matchCount == 2) + #expect(engine.activeRequest?.query.pattern == "new") + } + + @Test("An identical request while searching does not restart; a new content revision does") + func duplicateRequests() async { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock) + + engine.start(request("same", backend: backend)) + await backend.waitForRuns(1) + engine.start(request("same", backend: backend)) + #expect(backend.runCount == 1) + #expect(!backend.run(0).wasCancelled) + + engine.start(request("same", backend: backend, revision: 1)) + await backend.waitForRuns(2) + #expect(backend.run(0).wasCancelled) + engine.cancel(clearResults: true) + } + + @Test("Batches arriving within one frame are applied as one update") + func framePacing() async { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock, frame: .milliseconds(16)) + let recorder = EventRecorder() + recorder.attach(to: engine) + + engine.start(request("x", backend: backend)) + await backend.waitForRuns(1) + let run = backend.run(0) + + run.sink.send([group("/root/a", lines: [1])]) + await recorder.wait { if case .changed = $0 { true } else { false } } + await clock.waitForSleepers(1) + + run.sink.send([group("/root/a", lines: [2])]) + run.sink.send([group("/root/b", lines: [1])]) + run.sink.send([group("/root/c", lines: [1])]) + #expect(engine.tree.matchCount == 1) + + clock.advance(by: .milliseconds(16)) + await recorder.wait { if case .changed = $0 { true } else { false } } + + let changes = recorder.events.compactMap { event -> FileSearchTreeChange? in + if case .changed(let change) = event { change } else { nil } + } + #expect(changes.count == 2) + #expect(changes[1].grownFiles.map(\.fileIndex) == [0]) + #expect(changes[1].insertedFiles == 1..<3) + #expect(engine.tree.matchCount == 4) + engine.cancel(clearResults: true) + } + + @Test("A limited search reports the limit") + func limited() async { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock) + let recorder = EventRecorder() + recorder.attach(to: engine) + + engine.start(request("x", backend: backend)) + await backend.waitForRuns(1) + backend.complete(0, with: .limited(1_000)) + await recorder.wait { if case .phase(.finished) = $0 { true } else { false } } + + #expect(engine.phase == .finished(.limited(1_000))) + } + + @Test("Empty and invalid-regex queries never reach the backend") + func queriesThatDoNotRun() { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock) + + engine.start(request("", backend: backend)) + #expect(engine.phase == .idle) + + engine.start(request("(", backend: backend, isRegex: true)) + #expect(engine.phase == .finished(.failed(.invalidRegex(nil)))) + #expect(backend.runCount == 0) + } + + @Test("Cancel with clear stops the backend and empties the results") + func cancelClears() async { + let clock = ManualTestClock() + let backend = ScriptedBackend() + let engine = makeEngine(clock: clock) + + engine.start(request("x", backend: backend)) + await backend.waitForRuns(1) + engine.cancel(clearResults: true) + + #expect(backend.run(0).wasCancelled) + #expect(engine.phase == .idle) + #expect(engine.tree.isEmpty) + #expect(engine.activeRequest == nil) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchPerformanceTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchPerformanceTests.swift new file mode 100644 index 000000000000..f47c00cc1172 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchPerformanceTests.swift @@ -0,0 +1,102 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +/// 100,000 matches through the off-main decoder and the main-thread tree. +/// The numbers print so the PR can quote them; the bounds are loose enough +/// for a loaded CI runner and still catch an accidental quadratic path. +@Suite("100k-match throughput", .serialized) +struct FileSearchPerformanceTests { + static let fileCount = 2_000 + static let matchesPerFile = 50 + + static let output: Data = { + var text = "" + text.reserveCapacity(fileCount * matchesPerFile * 260) + let line = " let value = computeSomething(needle, other: needle) // trailing comment text\n" + for file in 0.. String { + let components = duration.components + let value = Double(components.seconds) * 1_000 + Double(components.attoseconds) / 1e15 + return String(format: "%.1f", value) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchProcessTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchProcessTests.swift new file mode 100644 index 000000000000..3b2125fa72da --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchProcessTests.swift @@ -0,0 +1,79 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +@Suite("Child process for searches", .serialized) +struct FileSearchProcessTests { + private func collect(_ process: FileSearchProcess) async -> Data { + var data = Data() + for await chunk in process.standardOutputChunks() { data.append(chunk) } + return data + } + + @Test("Arguments reach the child byte for byte, without NFD decomposition") + func argumentsAreNotNormalized() async throws { + let composed = "caf\u{E9}" // NFC "café" + let process = try FileSearchProcess(command: FileSearchCommand( + executablePath: "/bin/sh", + arguments: ["-c", "printf '%s' \"$1\"", "sh", composed] + )) + let output = await collect(process) + let exit = await process.waitForExit() + + #expect(exit.status == 0) + #expect(Array(output) == Array(composed.utf8)) + } + + @Test("Standard input is delivered and closed") + func standardInput() async throws { + let process = try FileSearchProcess(command: FileSearchCommand( + executablePath: "/bin/sh", + arguments: ["-s"], + standardInput: Data("printf 'from stdin'; exit 3\n".utf8) + )) + let output = await collect(process) + let exit = await process.waitForExit() + + #expect(String(decoding: output, as: UTF8.self) == "from stdin") + #expect(exit.status == 3) + } + + @Test("Terminate stops the whole process group, including grandchildren") + func terminateKillsGroup() async throws { + // The grandchild keeps stdout open; the stream can only end once it dies too. + let process = try FileSearchProcess(command: FileSearchCommand( + executablePath: "/bin/sh", + arguments: ["-c", "/bin/sleep 600 & echo ready; wait"] + )) + var iterator = process.standardOutputChunks().makeAsyncIterator() + let first = await iterator.next() + #expect(first.map { String(decoding: $0, as: UTF8.self) } == "ready\n") + + process.terminate() + while await iterator.next() != nil {} + let exit = await process.waitForExit() + #expect(exit.status == 128 + SIGTERM) + } + + @Test("A missing executable reports ripgrep as not found") + func missingExecutable() async { + let completion = await RipgrepStreamingSearch( + command: FileSearchCommand(executablePath: "/nonexistent/rg", arguments: []), + matchLimit: 10 + ).run(sink: FileSearchBatchMailbox()) + #expect(completion == .failed(.ripgrepNotFound)) + } + + @Test("Stderr is returned for diagnostics") + func standardErrorCaptured() async throws { + let process = try FileSearchProcess(command: FileSearchCommand( + executablePath: "/bin/sh", + arguments: ["-c", "echo boom >&2; exit 2"] + )) + _ = await collect(process) + let exit = await process.waitForExit() + #expect(exit.status == 2) + #expect(exit.standardError == "boom\n") + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchResultTreeTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchResultTreeTests.swift new file mode 100644 index 000000000000..9ab524d5f8b1 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchResultTreeTests.swift @@ -0,0 +1,138 @@ +import Testing + +@testable import CmuxFileSearch + +@Suite("Grouped result tree") +struct FileSearchResultTreeTests { + private func matches(_ lines: [Int]) -> [FileSearchMatch] { + lines.map { FileSearchMatch(lineNumber: $0, column: 1, length: 1, preview: "x", previewMatchRange: 0..<1) } + } + + private func makeTree() -> FileSearchResultTree { + FileSearchResultTree { path in String(path.dropFirst("/root/".count)) } + } + + @Test("Files group in first-seen order and report inserted rows") + func grouping() { + let tree = makeTree() + let change = tree.apply([ + FileSearchFileMatches(path: "/root/a", matches: matches([1, 2])), + FileSearchFileMatches(path: "/root/b", matches: matches([5])), + ]) + + #expect(change.insertedFiles == 0..<2) + #expect(change.grownFiles.isEmpty) + #expect(tree.files.map(\.relativePath) == ["a", "b"]) + #expect(tree.matchCount == 3) + #expect(tree.fileCount == 2) + } + + @Test("A file reported again merges into its existing row") + func merging() { + let tree = makeTree() + tree.apply([FileSearchFileMatches(path: "/root/a", matches: matches([1]))]) + let firstNode = tree.files[0] + let change = tree.apply([ + FileSearchFileMatches(path: "/root/a", matches: matches([7, 8])), + FileSearchFileMatches(path: "/root/c", matches: matches([2])), + ]) + + #expect(tree.files[0] === firstNode) + #expect(tree.files[0].matches.map(\.lineNumber) == [1, 7, 8]) + #expect(change.grownFiles.map(\.fileIndex) == [0]) + #expect(change.grownFiles.map(\.previousCount) == [1]) + #expect(change.insertedFiles == 1..<2) + #expect(tree.matchCount == 4) + } + + @Test("Match row objects are stable and point at their file") + func matchNodes() { + let tree = makeTree() + tree.apply([FileSearchFileMatches(path: "/root/a", matches: matches([1, 2]))]) + let node = tree.files[0].matchNode(at: 1) + + #expect(tree.files[0].matchNode(at: 1) === node) + #expect(node.match.lineNumber == 2) + #expect(tree.position(of: node) == FileSearchResultPosition(fileIndex: 0, matchIndex: 1)) + } + + @Test("Next and previous walk matches across files and wrap") + func navigation() { + let tree = makeTree() + tree.apply([ + FileSearchFileMatches(path: "/root/a", matches: matches([1, 2])), + FileSearchFileMatches(path: "/root/b", matches: matches([3])), + ]) + let a0 = FileSearchResultPosition(fileIndex: 0, matchIndex: 0) + let a1 = FileSearchResultPosition(fileIndex: 0, matchIndex: 1) + let b0 = FileSearchResultPosition(fileIndex: 1, matchIndex: 0) + + #expect(tree.nextMatch(after: nil) == a0) + #expect(tree.nextMatch(after: a0) == a1) + #expect(tree.nextMatch(after: a1) == b0) + #expect(tree.nextMatch(after: b0) == a0) + #expect(tree.nextMatch(after: FileSearchResultPosition(fileIndex: 1, matchIndex: nil)) == b0) + + #expect(tree.previousMatch(before: nil) == b0) + #expect(tree.previousMatch(before: b0) == a1) + #expect(tree.previousMatch(before: a0) == b0) + #expect(tree.previousMatch(before: FileSearchResultPosition(fileIndex: 1, matchIndex: nil)) == a1) + } + + @Test("Dismissing a file removes its matches and keeps later files addressable") + func dismiss() { + let tree = makeTree() + tree.apply([ + FileSearchFileMatches(path: "/root/a", matches: matches([1, 2])), + FileSearchFileMatches(path: "/root/b", matches: matches([3])), + ]) + let first = tree.files[0] + #expect(tree.remove(first)) + #expect(!tree.remove(first)) + #expect(tree.files.map(\.relativePath) == ["b"]) + #expect(tree.matchCount == 1) + #expect(tree.index(of: tree.files[0]) == 0) + let change = tree.apply([FileSearchFileMatches(path: "/root/b", matches: matches([4]))]) + #expect(change.grownFiles.map(\.fileIndex) == [0]) + } + + @Test("Navigation on an empty tree finds nothing") + func emptyNavigation() { + let tree = makeTree() + #expect(tree.nextMatch(after: nil) == nil) + #expect(tree.previousMatch(before: nil) == nil) + } +} + +@Suite("Search history") +struct FileSearchHistoryTests { + @Test("Recording deduplicates, keeps newest last and caps size") + func recording() { + var history = FileSearchHistory(capacity: 3) + for entry in ["a", "b", "a", "c", "d", ""] { history.record(entry) } + #expect(history.entries == ["a", "c", "d"]) + } + + @Test("Up walks back, down walks forward and restores the draft") + func cursor() { + let history = FileSearchHistory(entries: ["one", "two", "three"]) + var cursor = FileSearchHistoryCursor() + + #expect(cursor.previous(in: history, current: "dra") == "three") + #expect(cursor.previous(in: history, current: "three") == "two") + #expect(cursor.previous(in: history, current: "two") == "one") + #expect(cursor.previous(in: history, current: "one") == nil) + #expect(cursor.next(in: history) == "two") + #expect(cursor.next(in: history) == "three") + #expect(cursor.next(in: history) == "dra") + #expect(!cursor.isBrowsing) + #expect(cursor.next(in: history) == nil) + } + + @Test("Up skips the newest entry when the field already shows it") + func skipsCurrent() { + let history = FileSearchHistory(entries: ["one", "two"]) + var cursor = FileSearchHistoryCursor() + #expect(cursor.previous(in: history, current: "two") == "one") + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchRowListTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchRowListTests.swift new file mode 100644 index 000000000000..c1377c1d49cc --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/FileSearchRowListTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +@Suite("Flat result rows") +struct FileSearchRowListTests { + private func group(_ path: String, _ lines: [Int]) -> FileSearchFileMatches { + FileSearchFileMatches(path: path, matches: lines.map { + FileSearchMatch(lineNumber: $0, column: 1, length: 1, preview: "x", previewMatchRange: 0..<1) + }) + } + + @Test("Streaming appends rows and refreshes the grown file's row") + func appends() { + let tree = FileSearchResultTree { $0 } + let rows = FileSearchRowList(tree: tree) + #expect(rows.apply(tree.apply([group("a", [1, 2])])) == .appended(inserted: 0..<3, refreshedRows: [])) + #expect(rows.apply(tree.apply([group("a", [3]), group("b", [1])])) == + .appended(inserted: 3..<6, refreshedRows: IndexSet(integer: 0))) + #expect(rows.rows.map(\.matchIndex) == [nil, 0, 1, 2, nil, 0]) + } + + @Test("A file other than the last gaining matches reloads in order") + func earlierFileReloads() { + let tree = FileSearchResultTree { $0 } + let rows = FileSearchRowList(tree: tree) + _ = rows.apply(tree.apply([group("a", [1]), group("b", [1])])) + #expect(rows.apply(tree.apply([group("a", [2])])) == .reload) + #expect(rows.rows.map(\.fileIndex) == [0, 0, 0, 1, 1]) + } + + @Test("Collapsed files show only their file row; lookups find visible rows") + func collapse() { + let tree = FileSearchResultTree { $0 } + let rows = FileSearchRowList(tree: tree) + _ = rows.apply(tree.apply([group("a", [1, 2]), group("b", [1])])) + tree.files[0].isExpanded = false + rows.rebuild() + #expect(rows.count == 3) + #expect(rows.row(of: FileSearchResultPosition(fileIndex: 1, matchIndex: 0)) == 2) + #expect(rows.row(of: FileSearchResultPosition(fileIndex: 0, matchIndex: 1)) == nil) + #expect(rows.fileRow(containing: 2) == 1) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/ManualTestClock.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/ManualTestClock.swift new file mode 100644 index 000000000000..c207b3c7aecb --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/ManualTestClock.swift @@ -0,0 +1,86 @@ +import Foundation +import os + +/// A clock that only moves when a test advances it. Sleepers resume in +/// deadline order once the clock reaches their deadline; cancellation resumes +/// them immediately with `CancellationError`. +final class ManualTestClock: Clock, @unchecked Sendable { + struct Instant: InstantProtocol { + var offset: Duration + func advanced(by duration: Duration) -> Instant { Instant(offset: offset + duration) } + func duration(to other: Instant) -> Duration { other.offset - offset } + static func < (lhs: Instant, rhs: Instant) -> Bool { lhs.offset < rhs.offset } + } + + private struct Sleeper { + let id: UUID + let deadline: Instant + let continuation: CheckedContinuation + } + + private let lock = NSLock() + private var current = Instant(offset: .zero) + private var sleepers: [Sleeper] = [] + private var cancelledBeforeRegistering = Set() + private let sleeperCountStream = AsyncStream.makeStream() + + var now: Instant { + lock.lock() + defer { lock.unlock() } + return current + } + + var minimumResolution: Duration { .zero } + + func sleep(until deadline: Instant, tolerance: Duration?) async throws { + let id = UUID() + try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + lock.lock() + if cancelledBeforeRegistering.remove(id) != nil { + lock.unlock() + continuation.resume(throwing: CancellationError()) + return + } + if deadline <= current { + lock.unlock() + continuation.resume() + return + } + sleepers.append(Sleeper(id: id, deadline: deadline, continuation: continuation)) + let count = sleepers.count + lock.unlock() + sleeperCountStream.continuation.yield(count) + } + } onCancel: { + lock.lock() + let index = sleepers.firstIndex { $0.id == id } + let sleeper = index.map { sleepers.remove(at: $0) } + if sleeper == nil { cancelledBeforeRegistering.insert(id) } + lock.unlock() + sleeper?.continuation.resume(throwing: CancellationError()) + } + } + + /// Moves time forward and wakes every sleeper whose deadline passed. + func advance(by duration: Duration) { + lock.lock() + current = current.advanced(by: duration) + let due = sleepers.filter { $0.deadline <= current }.sorted { $0.deadline < $1.deadline } + sleepers.removeAll { $0.deadline <= current } + lock.unlock() + for sleeper in due { sleeper.continuation.resume() } + } + + var sleeperCount: Int { + lock.lock() + defer { lock.unlock() } + return sleepers.count + } + + /// Returns once at least `count` sleepers are waiting. + func waitForSleepers(_ count: Int) async { + if sleeperCount >= count { return } + for await value in sleeperCountStream.stream where value >= count { return } + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepArgumentsTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepArgumentsTests.swift new file mode 100644 index 000000000000..9abf825f9163 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepArgumentsTests.swift @@ -0,0 +1,86 @@ +import Testing + +@testable import CmuxFileSearch + +@Suite("Query options to ripgrep arguments") +struct RipgrepArgumentsTests { + private func globs(_ arguments: [String]) -> [String] { + stride(from: 0, to: arguments.count - 1, by: 1) + .filter { arguments[$0] == "--glob" } + .map { arguments[$0 + 1] } + } + + @Test("Default query: literal, case-insensitive, ignore files honored") + func defaults() { + let arguments = FileSearchQuery(pattern: "needle").ripgrepArguments(rootPath: "/root") + + #expect(arguments.contains("--json")) + #expect(arguments.contains("--no-config")) + #expect(arguments.contains("--hidden")) + #expect(arguments.contains("--ignore-case")) + #expect(arguments.contains("--fixed-strings")) + #expect(!arguments.contains("--word-regexp")) + #expect(!arguments.contains("--no-ignore")) + #expect(globs(arguments) == ["!.git", "!node_modules", "!dist", "!build", "!DerivedData"]) + #expect(Array(arguments.suffix(3)) == ["--", "needle", "/root"]) + } + + @Test("Match Case, Whole Word and Regex map to their flags") + func toggles() { + let query = FileSearchQuery(pattern: "a.b", isCaseSensitive: true, matchesWholeWord: true, isRegex: true) + let arguments = query.ripgrepArguments(rootPath: "/root") + + #expect(arguments.contains("--case-sensitive")) + #expect(!arguments.contains("--ignore-case")) + #expect(arguments.contains("--word-regexp")) + #expect(arguments.contains("--auto-hybrid-regex")) + #expect(!arguments.contains("--fixed-strings")) + } + + @Test("Turning off ignore files searches ignored and generated folders but never .git") + func ignoreToggleOff() { + let arguments = FileSearchQuery(pattern: "x", usesIgnoreFiles: false).ripgrepArguments(rootPath: "/root") + + #expect(arguments.contains("--no-ignore")) + #expect(globs(arguments) == ["!.git"]) + } + + @Test("A pattern that looks like a flag stays the pattern") + func dashPattern() { + let arguments = FileSearchQuery(pattern: "--files").ripgrepArguments(rootPath: "/root") + #expect(Array(arguments.suffix(3)) == ["--", "--files", "/root"]) + } + + @Test("Include and exclude fields become globs", arguments: [ + ("*.swift", ["*.swift"]), + ("src", ["src", "**/src/**"]), + ("./src/app/", ["src/app", "src/app/**"]), + ("*.{ts,tsx}, docs", ["*.{ts,tsx}", "docs", "**/docs/**"]), + (" , ,", []), + ("**/*.test.js", ["**/*.test.js"]), + ]) + func includeGlobs(field: String, expected: [String]) { + let query = FileSearchQuery(pattern: "x", includePatterns: field, usesIgnoreFiles: false) + let arguments = query.ripgrepArguments(rootPath: "/root") + #expect(globs(arguments) == ["!.git"] + expected) + } + + @Test("Exclude entries are negated") + func excludeGlobs() { + let query = FileSearchQuery(pattern: "x", excludePatterns: "vendor, *.min.js", usesIgnoreFiles: false) + let arguments = query.ripgrepArguments(rootPath: "/root") + #expect(globs(arguments) == ["!.git", "!vendor", "!**/vendor/**", "!*.min.js"]) + } + + @Test("Commas inside braces do not split entries") + func braceSplitting() { + #expect(FileSearchGlobList("a,{b,c},d").entries == ["a", "{b,c}", "d"]) + } + + @Test("The local regex precheck rejects what cannot compile") + func regexPrecheck() { + #expect(FileSearchQuery(pattern: "(", isRegex: true).regexSyntaxError != nil) + #expect(FileSearchQuery(pattern: "(", isRegex: false).regexSyntaxError == nil) + #expect(FileSearchQuery(pattern: "a+b", isRegex: true).regexSyntaxError == nil) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepIntegrationTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepIntegrationTests.swift new file mode 100644 index 000000000000..b26871d6c8e9 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepIntegrationTests.swift @@ -0,0 +1,101 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +enum InstalledRipgrep { + static let path: String? = [ + "/opt/homebrew/bin/rg", "/usr/local/bin/rg", "/usr/bin/rg", + ].first { FileManager.default.isExecutableFile(atPath: $0) } +} + +/// Runs the real ripgrep over a scratch tree, so the option mapping is +/// proven by what ripgrep actually returns rather than by the argv shape. +@Suite("ripgrep end to end", .serialized, .enabled(if: InstalledRipgrep.path != nil, "needs ripgrep")) +struct RipgrepIntegrationTests { + private let root: URL + + init() throws { + root = FileManager.default.temporaryDirectory.appendingPathComponent("cmux-file-search-\(UUID().uuidString)") + let files: [String: String] = [ + "src/app.swift": "let Needle = 1\nlet needle = 2\nneedles are sharp\n", + "src/app.test.swift": "needle in test\n", + "docs/readme.md": "a needle in docs\n", + "node_modules/dep/index.js": "needle in dependency\n", + "ignored/secret.txt": "needle ignored by gitignore\n", + ".gitignore": "ignored/\n", + ".env": "needle=hidden file\n", + ".git/config": "needle inside git\n", + ] + for (path, contents) in files { + let url = root.appendingPathComponent(path) + try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) + try contents.write(to: url, atomically: true, encoding: .utf8) + } + // ripgrep honors .gitignore only inside a repository. + try FileManager.default.createDirectory(at: root.appendingPathComponent(".git/objects"), withIntermediateDirectories: true) + } + + private func search(_ query: FileSearchQuery) async throws -> (files: [String: Int], completion: FileSearchCompletion) { + let rg = try #require(InstalledRipgrep.path) + defer { try? FileManager.default.removeItem(at: root) } + let mailbox = FileSearchBatchMailbox() + let completion = await RipgrepStreamingSearch(command: FileSearchCommand(executablePath: rg, arguments: query.ripgrepArguments(rootPath: root.path)), matchLimit: 1_000).run(sink: mailbox) + var counts: [String: Int] = [:] + for group in mailbox.drain().groups { + counts[String(group.path.dropFirst(root.path.count + 1)), default: 0] += group.matches.count + } + return (counts, completion) + } + + @Test("Defaults: case-insensitive literal, hidden files, ignore rules, never .git") + func defaults() async throws { + let result = try await search(FileSearchQuery(pattern: "needle")) + #expect(result.completion == .completed) + #expect(result.files == ["src/app.swift": 3, "src/app.test.swift": 1, "docs/readme.md": 1, ".env": 1]) + } + + @Test("Match Case") + func matchCase() async throws { + let result = try await search(FileSearchQuery(pattern: "Needle", isCaseSensitive: true)) + #expect(result.files == ["src/app.swift": 1]) + } + + @Test("Match Whole Word") + func wholeWord() async throws { + let result = try await search(FileSearchQuery(pattern: "needle", isCaseSensitive: true, matchesWholeWord: true, includePatterns: "src")) + #expect(result.files == ["src/app.swift": 1, "src/app.test.swift": 1]) + } + + @Test("Regex with an invalid pattern reports ripgrep's parse error") + func invalidRegex() async throws { + let result = try await search(FileSearchQuery(pattern: "needle(", isRegex: true)) + guard case .failed(.invalidRegex(let detail)) = result.completion else { + Issue.record("expected invalidRegex, got \(result.completion)") + return + } + #expect(detail?.isEmpty == false) + } + + @Test("Include and exclude globs") + func globs() async throws { + let result = try await search(FileSearchQuery(pattern: "needle", includePatterns: "src", excludePatterns: "*.test.swift")) + #expect(result.files == ["src/app.swift": 3]) + } + + @Test("Turning off ignore files finds ignored and generated folders") + func ignoreFilesOff() async throws { + let result = try await search(FileSearchQuery(pattern: "needle", usesIgnoreFiles: false)) + #expect(result.files["ignored/secret.txt"] == 1) + #expect(result.files["node_modules/dep/index.js"] == 1) + #expect(result.files[".git/config"] == nil) + } + + @Test("A non-ASCII pattern matches NFC file contents") + func unicodePattern() async throws { + let url = root.appendingPathComponent("unicode.txt") + try "caf\u{E9} au lait\n".write(to: url, atomically: true, encoding: .utf8) + let result = try await search(FileSearchQuery(pattern: "caf\u{E9}")) + #expect(result.files["unicode.txt"] == 1) + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepJSONLineParserTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepJSONLineParserTests.swift new file mode 100644 index 000000000000..a7c47775e5e0 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepJSONLineParserTests.swift @@ -0,0 +1,160 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +@Suite("ripgrep JSON line parsing") +struct RipgrepJSONLineParserTests { + @Test("A match line yields a 1-based UTF-16 column and a trimmed preview") + func singleMatch() throws { + let line = RipgrepFixture.matchLine( + path: "/tmp/project/Sources/App.swift", + line: " let title = \"Search files\"\n", + lineNumber: 42, + byteRanges: [17..<23] + ) + let group = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))) + + #expect(group.path == "/tmp/project/Sources/App.swift") + #expect(group.matches.count == 1) + let match = try #require(group.matches.first) + #expect(match.lineNumber == 42) + #expect(match.column == 18) + #expect(match.length == 6) + #expect(match.preview == "let title = \"Search files\"") + #expect(match.highlightedText == "Search") + } + + @Test("Every submatch on a line becomes its own match") + func multipleSubmatches() throws { + let line = RipgrepFixture.matchLine( + path: "/p/a.txt", + line: "foo bar foo baz foo\n", + lineNumber: 3, + byteRanges: [0..<3, 8..<11, 16..<19] + ) + let group = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))) + + #expect(group.matches.map(\.column) == [1, 9, 17]) + #expect(group.matches.map(\.lineNumber) == [3, 3, 3]) + #expect(group.matches.map(\.highlightedText) == ["foo", "foo", "foo"]) + } + + @Test("Byte offsets after multibyte characters convert to UTF-16 columns") + func multibyteOffsets() throws { + // "é" is 2 UTF-8 bytes and 1 UTF-16 unit; "😀" is 4 bytes and 2 units. + let text = "é😀 needle\n" + let start = Array("é😀 ".utf8).count + let line = RipgrepFixture.matchLine( + path: "/p/u.txt", + line: text, + lineNumber: 1, + byteRanges: [start..<(start + 6)] + ) + let match = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))?.matches.first) + + #expect(match.column == 5) + #expect(match.length == 6) + #expect(match.highlightedText == "needle") + } + + @Test("Valid UTF-8 bytes payloads decode like text payloads") + func bytesPayload() throws { + let line = try RipgrepFixture.matchLine( + pathPayload: ["bytes": Data("/p/Bytes.swift".utf8).base64EncodedString()], + linesPayload: ["bytes": Data("let needle = 1\n".utf8).base64EncodedString()], + lineNumber: 7, + byteRanges: [4..<10] + ) + let group = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))) + + #expect(group.path == "/p/Bytes.swift") + #expect(group.matches.first?.column == 5) + #expect(group.matches.first?.highlightedText == "needle") + } + + @Test("Invalid UTF-8 becomes U+FFFD without shifting the highlight") + func invalidUTF8Bytes() throws { + // 0x80 is a lone continuation byte before the match "oo". + let bytes: [UInt8] = [0x66, 0x80, 0x6F, 0x6F, 0x0A] + let line = try RipgrepFixture.matchLine( + pathPayload: ["text": "/p/Invalid.bin"], + linesPayload: ["bytes": Data(bytes).base64EncodedString()], + lineNumber: 1, + byteRanges: [2..<4] + ) + let match = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))?.matches.first) + + #expect(match.preview.unicodeScalars.map(\.value) == [0x66, 0xFFFD, 0x6F, 0x6F]) + #expect(match.column == 3) + #expect(match.highlightedText == "oo") + } + + @Test("A long prefix is elided and the preview stays bounded") + func longLineIsTrimmedAroundMatch() throws { + let prefix = String(repeating: "a", count: 5_000) + let suffix = String(repeating: "b", count: 5_000) + let text = prefix + "needle" + suffix + "\n" + let line = RipgrepFixture.matchLine(path: "/p/min.js", line: text, lineNumber: 1, byteRanges: [5_000..<5_006]) + let match = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))?.matches.first) + + #expect(match.column == 5_001) + #expect(match.preview.hasPrefix("\u{2026}")) + #expect(match.highlightedText == "needle") + #expect(match.preview.utf16.count <= RipgrepJSONLineParser().previewMaximumLength + 1) + #expect(match.previewMatchRange.lowerBound == RipgrepJSONLineParser().previewLeadingContext + 1) + } + + @Test("Line endings are dropped and tabs render as spaces") + func lineEndingsAndTabs() throws { + let line = RipgrepFixture.matchLine(path: "/p/t.txt", line: "\tx\tneedle\r\n", lineNumber: 2, byteRanges: [3..<9]) + let match = try #require(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8))?.matches.first) + + #expect(match.preview == "x needle") + #expect(match.column == 4) + #expect(match.highlightedText == "needle") + } + + @Test("Non-match events are ignored", arguments: [ + #"{"type":"begin","data":{"path":{"text":"/p/a"}}}"#, + #"{"type":"end","data":{"path":{"text":"/p/a"},"binary_offset":null,"stats":{}}}"#, + #"{"type":"summary","data":{"elapsed_total":{"secs":0,"nanos":1}}}"#, + #"{"type":"match","data":{"broken":true}}"#, + "not json", + ]) + func ignoresOtherEvents(line: String) { + #expect(RipgrepJSONLineParser().parseMatch(line: Array(line.utf8)) == nil) + } +} + +extension FileSearchMatch { + var highlightedText: String { + let units = Array(preview.utf16) + return String(decoding: units[previewMatchRange], as: UTF16.self) + } +} + +enum RipgrepFixture { + static func matchLine(path: String, line: String, lineNumber: Int, byteRanges: [Range]) -> String { + // swiftlint:disable:next force_try + try! matchLine(pathPayload: ["text": path], linesPayload: ["text": line], lineNumber: lineNumber, byteRanges: byteRanges) + } + + static func matchLine( + pathPayload: [String: Any], + linesPayload: [String: Any], + lineNumber: Int, + byteRanges: [Range] + ) throws -> String { + let payload: [String: Any] = [ + "path": pathPayload, + "lines": linesPayload, + "line_number": lineNumber, + "absolute_offset": 0, + "submatches": byteRanges.map { ["match": ["text": ""], "start": $0.lowerBound, "end": $0.upperBound] }, + ] + let data = try JSONSerialization.data(withJSONObject: payload) + // ripgrep always writes the type key first. + return #"{"type":"match","data":"# + String(decoding: data, as: UTF8.self) + "}" + } +} diff --git a/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepStreamDecoderTests.swift b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepStreamDecoderTests.swift new file mode 100644 index 000000000000..b2d4e7eeb019 --- /dev/null +++ b/Packages/macOS/CmuxFileSearch/Tests/CmuxFileSearchTests/RipgrepStreamDecoderTests.swift @@ -0,0 +1,82 @@ +import Foundation +import Testing + +@testable import CmuxFileSearch + +@Suite("Streaming ripgrep output") +struct RipgrepStreamDecoderTests { + private func output(files: [(String, Int)]) -> Data { + var text = "" + for (path, count) in files { + text += #"{"type":"begin","data":{"path":{"text":"\#(path)"}}}"# + "\n" + for line in 1...count { + text += RipgrepFixture.matchLine(path: path, line: "hit needle\n", lineNumber: line, byteRanges: [4..<10]) + "\n" + } + text += #"{"type":"end","data":{"path":{"text":"\#(path)"}}}"# + "\n" + } + return Data(text.utf8) + } + + @Test("Lines split across chunk borders decode once and stay grouped") + func byteAtATime() { + let data = output(files: [("/r/a", 3), ("/r/b", 2)]) + let decoder = RipgrepStreamDecoder(matchLimit: 100) + var groups: [FileSearchFileMatches] = [] + for byte in data { + groups.appendMerging(decoder.consume([byte])) + } + groups.appendMerging(decoder.finish()) + + #expect(groups.map(\.path) == ["/r/a", "/r/b"]) + #expect(groups.map(\.matches.count) == [3, 2]) + #expect(decoder.matchCount == 5) + #expect(!decoder.isLimitReached) + } + + @Test("An unterminated final line is decoded by finish()") + func trailingLine() { + var data = output(files: [("/r/a", 1)]) + let match = RipgrepFixture.matchLine(path: "/r/z", line: "needle", lineNumber: 9, byteRanges: [0..<6]) + data.append(contentsOf: Array(match.utf8)) + let decoder = RipgrepStreamDecoder(matchLimit: 100) + var groups = decoder.consume(data) + groups.appendMerging(decoder.finish()) + + #expect(groups.map(\.path) == ["/r/a", "/r/z"]) + } + + @Test("Exactly the limit completes; one more is limited and truncated") + func limit() { + let exact = RipgrepStreamDecoder(matchLimit: 4) + _ = exact.consume(output(files: [("/r/a", 4)])) + #expect(exact.matchCount == 4) + #expect(!exact.isLimitReached) + + let over = RipgrepStreamDecoder(matchLimit: 4) + let groups = over.consume(output(files: [("/r/a", 3), ("/r/b", 3)])) + #expect(over.isLimitReached) + #expect(over.matchCount == 4) + #expect(groups.map(\.matches.count) == [3, 1]) + #expect(over.consume(output(files: [("/r/c", 1)])).isEmpty) + } + + @Test("Exit statuses classify into completions", arguments: [ + (Int32(0), "", 3, false, FileSearchCompletion.completed), + (1, "", 0, false, .completed), + (2, "permission denied", 5, false, .completed), + (2, "some failure", 0, false, .failed(.processFailed(status: 2, message: "some failure"))), + (2, "regex parse error:\n (\n ^", 0, false, .failed(.invalidRegex("regex parse error:\n (\n ^"))), + (127, "cmux-file-search: rg not found", 0, false, .failed(.ripgrepNotFound)), + (143, "", 10, true, .limited(10)), + ]) + func classification(status: Int32, stderr: String, matches: Int, limited: Bool, expected: FileSearchCompletion) { + let completion = FileSearchCompletion( + ripgrepExitStatus: status, + standardError: stderr, + matchCount: matches, + limitReached: limited, + matchLimit: 10 + ) + #expect(completion == expected) + } +} diff --git a/Packages/macOS/CmuxFileTree/Package.swift b/Packages/macOS/CmuxFileTree/Package.swift new file mode 100644 index 000000000000..d661e0337b9c --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "CmuxFileTree", + platforms: [ + .macOS(.v14), + ], + products: [ + .library( + name: "CmuxFileTree", + targets: ["CmuxFileTree"] + ), + ], + targets: [ + .target( + name: "CmuxFileTree", + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + ] + ), + .testTarget( + name: "CmuxFileTreeTests", + dependencies: ["CmuxFileTree"], + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + ] + ), + ] +) diff --git a/Packages/macOS/CmuxFileTree/README.md b/Packages/macOS/CmuxFileTree/README.md new file mode 100644 index 000000000000..67dd35171401 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/README.md @@ -0,0 +1,28 @@ +# CmuxFileTree + +`CmuxFileTree` is the model behind the right sidebar Files tree. It has no +AppKit dependency and owns everything that should not run on the main actor: + +- `FileTreeProvider`: one protocol for this Mac, SSH hosts and Cloud VMs. It + lists directories (with a batch call so SSH restores expanded folders in one + round trip) and optionally streams change batches. +- `LocalFileTreeProvider`: `getattrlistbulk(2)` listing and one FSEvents stream + per root. +- `FileTreeEngine`: an actor that caches raw listings, sorts and filters them, + drops superseded loads and delivers `FileTreeChildrenDiff` values shaped for + `NSOutlineView` batch updates. +- `FileTreeSortOrder`: Finder-like natural name order plus kind, date and size. + +Tests inject a scripted provider; no test touches `UserDefaults.standard` or the +user's files: + +```swift +let engine = FileTreeEngine(provider: LocalFileTreeProvider()) +let updates = await engine.load([temporaryDirectory.path]) +``` + +```bash +swift test --package-path Packages/macOS/CmuxFileTree +swift test --package-path Packages/macOS/CmuxFileTree -c release -Xswiftc -enable-testing \ + --filter FileTreePerformanceBenchmarkTests # 50k-entry benchmark numbers +``` diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/DirectoryBulkReader.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/DirectoryBulkReader.swift new file mode 100644 index 000000000000..72b9ade2dd43 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/DirectoryBulkReader.swift @@ -0,0 +1,136 @@ +import Darwin +import Foundation + +/// Reads a local directory with `getattrlistbulk(2)`. +/// +/// One syscall returns names, types, sizes, times and flags for a buffer of +/// entries. The previous `contentsOfDirectory` plus one `fileExists` per entry +/// paid a `stat` per row; for a 50,000-entry `node_modules` that was the +/// difference between roughly 700 ms and a few tens of milliseconds. +struct DirectoryBulkReader: Sendable { + /// The per-call attribute buffer. Large enough for about 2,000 entries. + private let bufferSize: Int + + /// Creates a reader. + /// - Parameter bufferSize: The attribute buffer size in bytes. + init(bufferSize: Int = 256 * 1024) { + self.bufferSize = bufferSize + } + + /// Lists `path`, hidden entries included. + /// - Parameter path: An absolute directory path. + /// - Returns: The entries in filesystem order. + /// - Throws: `POSIXError` when the directory cannot be opened or read. + func entries(atPath path: String) throws -> [FileTreeEntry] { + let fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC) + guard fd >= 0 else { throw Self.posixError(errno) } + defer { close(fd) } + + var request = attrlist() + request.bitmapcount = u_short(ATTR_BIT_MAP_COUNT) + request.commonattr = attrgroup_t(truncatingIfNeeded: ATTR_CMN_RETURNED_ATTRS) | attrgroup_t(truncatingIfNeeded: ATTR_CMN_NAME) | + attrgroup_t(truncatingIfNeeded: ATTR_CMN_ERROR) | attrgroup_t(truncatingIfNeeded: ATTR_CMN_OBJTYPE) | attrgroup_t(truncatingIfNeeded: ATTR_CMN_MODTIME) | + attrgroup_t(truncatingIfNeeded: ATTR_CMN_FLAGS) + request.fileattr = attrgroup_t(truncatingIfNeeded: ATTR_FILE_DATALENGTH) + + let buffer = UnsafeMutableRawPointer.allocate(byteCount: bufferSize, alignment: 16) + defer { buffer.deallocate() } + + let parentPrefix = path.hasSuffix("/") ? path : path + "/" + var result: [FileTreeEntry] = [] + while true { + let count = getattrlistbulk(fd, &request, buffer, bufferSize, 0) + if count < 0 { + let code = errno + if code == EINTR { continue } + throw Self.posixError(code) + } + if count == 0 { break } + var cursor = buffer + for _ in 0.. FileTreeEntry? { + var field = start.advanced(by: MemoryLayout.size) + let returned = field.loadUnaligned(as: attribute_set_t.self) + field = field.advanced(by: MemoryLayout.size) + + // Layout per getattrlistbulk(2): the error code precedes the name. + var entryError: UInt32 = 0 + if returned.commonattr & attrgroup_t(truncatingIfNeeded: ATTR_CMN_ERROR) != 0 { + entryError = field.loadUnaligned(as: UInt32.self) + field = field.advanced(by: MemoryLayout.size) + } + guard returned.commonattr & attrgroup_t(truncatingIfNeeded: ATTR_CMN_NAME) != 0 else { return nil } + let nameReference = field.loadUnaligned(as: attrreference_t.self) + let namePointer = field.advanced(by: Int(nameReference.attr_dataoffset)) + .assumingMemoryBound(to: CChar.self) + let name = String(cString: namePointer) + field = field.advanced(by: MemoryLayout.size) + guard name != ".", name != ".." else { return nil } + let path = parentPrefix + name + if entryError != 0 { + return FileTreeEntry(name: name, path: path, kind: .other) + } + + var objectType = UInt32(VNON.rawValue) + if returned.commonattr & attrgroup_t(truncatingIfNeeded: ATTR_CMN_OBJTYPE) != 0 { + objectType = field.loadUnaligned(as: UInt32.self) + field = field.advanced(by: MemoryLayout.size) + } + var modificationTime: TimeInterval? + if returned.commonattr & attrgroup_t(truncatingIfNeeded: ATTR_CMN_MODTIME) != 0 { + let time = field.loadUnaligned(as: timespec.self) + modificationTime = TimeInterval(time.tv_sec) + TimeInterval(time.tv_nsec) / 1_000_000_000 + field = field.advanced(by: MemoryLayout.size) + } + var flags: UInt32 = 0 + if returned.commonattr & attrgroup_t(truncatingIfNeeded: ATTR_CMN_FLAGS) != 0 { + flags = field.loadUnaligned(as: UInt32.self) + field = field.advanced(by: MemoryLayout.size) + } + var size: Int64? + if returned.fileattr & attrgroup_t(truncatingIfNeeded: ATTR_FILE_DATALENGTH) != 0 { + size = field.loadUnaligned(as: off_t.self) + } + + let kind: FileTreeEntryKind + switch objectType { + case UInt32(VDIR.rawValue): + kind = .directory + case UInt32(VREG.rawValue): + kind = .file + case UInt32(VLNK.rawValue): + kind = Self.symbolicLinkKind(path: path) + default: + kind = .other + } + return FileTreeEntry( + name: name, + path: path, + kind: kind, + size: kind == .file ? size : nil, + modificationTime: modificationTime, + hasHiddenFlag: flags & UInt32(UF_HIDDEN) != 0 + ) + } + + /// Follows one symlink to decide whether it expands like a folder. + private static func symbolicLinkKind(path: String) -> FileTreeEntryKind { + var info = stat() + guard stat(path, &info) == 0 else { return .symbolicLink } + return (info.st_mode & S_IFMT) == S_IFDIR ? .symbolicLinkToDirectory : .symbolicLink + } + + private static func posixError(_ code: Int32) -> POSIXError { + POSIXError(POSIXErrorCode(rawValue: code) ?? .EIO) + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileSystemEventStream.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileSystemEventStream.swift new file mode 100644 index 000000000000..935bda442257 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileSystemEventStream.swift @@ -0,0 +1,156 @@ +import CoreServices +import Darwin +import Foundation + +/// Delivers FSEvents for one local root as ``FileTreeChangeBatch`` values. +/// +/// FSEvents reports directories, coalesces over ``latency`` in the kernel and +/// covers the whole subtree with one stream, unlike a `DispatchSource` per +/// directory. Event paths arrive canonicalized (`/private/tmp`, resolved +/// symlinks); the stream maps them back under the root the tree displays. +struct FileSystemEventStream: Sendable { + /// The displayed root. + let rootPath: String + /// The kernel coalescing window in seconds. + let latency: TimeInterval + + /// Creates a stream description. + /// - Parameters: + /// - rootPath: The absolute root the tree displays. + /// - latency: FSEvents coalescing in seconds. A build writing thousands of + /// files becomes a few batches per second. + init(rootPath: String, latency: TimeInterval = 0.25) { + self.rootPath = rootPath + self.latency = latency + } + + /// Starts FSEvents and returns its batches. Cancelling or dropping the + /// consumer stops and releases the stream. + func makeStream() -> AsyncStream { + let (stream, continuation) = AsyncStream.makeStream( + bufferingPolicy: .bufferingNewest(64) + ) + let displayRoot = Self.trimmedDirectory(rootPath) + let canonicalRoot = Self.canonicalPath(displayRoot) + let box = FileSystemEventStreamBox( + continuation: continuation, + displayRoot: displayRoot, + canonicalRoot: canonicalRoot + ) + // FSEvents needs a serial dispatch queue for delivery; it is the event + // source, not a lock around shared state (concurrency carve-out). + let queue = DispatchQueue(label: "cmux.file-tree.fsevents", qos: .utility) + var context = FSEventStreamContext( + version: 0, + info: Unmanaged.passRetained(box).toOpaque(), + retain: nil, + release: { info in + guard let info else { return } + Unmanaged.fromOpaque(info).release() + }, + copyDescription: nil + ) + let flags = FSEventStreamCreateFlags( + kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagWatchRoot | kFSEventStreamCreateFlagNoDefer + ) + guard let eventStream = FSEventStreamCreate( + kCFAllocatorDefault, + FileSystemEventStreamBox.callback, + &context, + [canonicalRoot] as CFArray, + FSEventStreamEventId(kFSEventStreamEventIdSinceNow), + latency, + flags + ) else { + // FSEventStreamCreate copied nothing; balance the retain above. + Unmanaged.fromOpaque(context.info!).release() + continuation.finish() + return stream + } + let handle = FileSystemEventStreamHandle(stream: eventStream, queue: queue) + FSEventStreamSetDispatchQueue(eventStream, queue) + FSEventStreamStart(eventStream) + continuation.onTermination = { _ in + handle.stop() + } + return stream + } + + static func trimmedDirectory(_ path: String) -> String { + guard path.count > 1, path.hasSuffix("/") else { return path } + return String(path.dropLast()) + } + + static func canonicalPath(_ path: String) -> String { + guard let resolved = realpath(path, nil) else { return path } + defer { free(resolved) } + return String(cString: resolved) + } +} + +/// Owns the FSEvents stream reference so termination can stop it on its queue. +private final class FileSystemEventStreamHandle: @unchecked Sendable { + // Accessed only on `queue` after creation; the reference is immutable. + private let stream: FSEventStreamRef + private let queue: DispatchQueue + + init(stream: FSEventStreamRef, queue: DispatchQueue) { + self.stream = stream + self.queue = queue + } + + func stop() { + let stream = self.stream + queue.async { + FSEventStreamStop(stream) + FSEventStreamInvalidate(stream) + FSEventStreamRelease(stream) + } + } +} + +/// Context object handed to the C callback. +private final class FileSystemEventStreamBox: Sendable { + let continuation: AsyncStream.Continuation + let displayRoot: String + let canonicalRoot: String + + init(continuation: AsyncStream.Continuation, displayRoot: String, canonicalRoot: String) { + self.continuation = continuation + self.displayRoot = displayRoot + self.canonicalRoot = canonicalRoot + } + + /// FSEvents' C callback; a `@convention(c)` trampoline is the one place a + /// free function is required, so it lives here as a static closure. + static let callback: FSEventStreamCallback = { _, info, count, paths, flags, _ in + guard let info else { return } + let box = Unmanaged.fromOpaque(info).takeUnretainedValue() + let array = Unmanaged.fromOpaque(paths).takeUnretainedValue() as NSArray + var batch = FileTreeChangeBatch() + for index in 0.. String { + if path == canonicalRoot { return displayRoot } + let prefix = canonicalRoot == "/" ? "/" : canonicalRoot + "/" + guard path.hasPrefix(prefix) else { return path } + let suffix = path.dropFirst(prefix.count) + return displayRoot == "/" ? "/" + suffix : displayRoot + "/" + suffix + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeChangeBatch.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeChangeBatch.swift new file mode 100644 index 000000000000..5d2de6ee91ee --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeChangeBatch.swift @@ -0,0 +1,31 @@ +/// Directories whose contents changed, coalesced over one delivery window. +/// +/// A batch names directories, not files: the tree re-lists a changed directory +/// only when it is loaded, so churn inside collapsed or never-opened folders +/// (a build writing `node_modules` or `.build`) costs one set lookup per path. +public struct FileTreeChangeBatch: Sendable, Equatable { + /// Directories whose direct children may have changed. + public var directories: Set + /// Directories whose whole subtree must be re-listed because the event + /// source dropped events (FSEvents `MustScanSubDirs`, a kernel overflow). + public var subtrees: Set + + /// Creates a batch. + /// - Parameters: + /// - directories: Directories whose direct children changed. + /// - subtrees: Directories whose entire subtree is stale. + public init(directories: Set = [], subtrees: Set = []) { + self.directories = directories + self.subtrees = subtrees + } + + /// Whether the batch names nothing. + public var isEmpty: Bool { directories.isEmpty && subtrees.isEmpty } + + /// Adds another batch's directories to this one. + /// - Parameter other: The batch to merge. + public mutating func formUnion(_ other: FileTreeChangeBatch) { + directories.formUnion(other.directories) + subtrees.formUnion(other.subtrees) + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeChildrenDiff.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeChildrenDiff.swift new file mode 100644 index 000000000000..a2c85e7d568f --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeChildrenDiff.swift @@ -0,0 +1,156 @@ +public import Foundation + +/// The row changes that turn one sorted child list into another. +/// +/// The indices are shaped for `NSOutlineView` batch updates: apply +/// ``removed`` (old coordinates) with `removeItems(at:inParent:)`, then +/// ``inserted`` (new coordinates) with `insertItems(at:inParent:)`, inside +/// one `beginUpdates()`/`endUpdates()` pair. A row whose path survives keeps +/// its identity, so AppKit keeps its selection and expansion; only its cell +/// needs a refresh when ``updated`` lists it. +/// +/// ```swift +/// let diff = FileTreeChildrenDiff(old: previous, new: next) +/// outlineView.beginUpdates() +/// outlineView.removeItems(at: diff.removed, inParent: parent, withAnimation: []) +/// outlineView.insertItems(at: diff.inserted, inParent: parent, withAnimation: []) +/// outlineView.endUpdates() +/// ``` +public struct FileTreeChildrenDiff: Sendable, Equatable { + /// Indices into the old list to remove, including rows that moved. + public let removed: IndexSet + /// Indices into the new list to insert, including rows that moved. + public let inserted: IndexSet + /// Indices into the new list whose path stayed in place but whose entry + /// changed (size, time, kind). Their cells need reconfiguring. + public let updated: IndexSet + /// Paths that moved position. They appear in both ``removed`` and + /// ``inserted``; the owner reuses the row object so identity survives. + public let moved: Set + + /// A diff with no changes. + public static let empty = FileTreeChildrenDiff(removed: [], inserted: [], updated: [], moved: []) + + /// Creates a diff from explicit index sets. + /// - Parameters: + /// - removed: Old-coordinate indices to remove. + /// - inserted: New-coordinate indices to insert. + /// - updated: New-coordinate indices whose content changed in place. + /// - moved: Paths that are removed and re-inserted. + public init(removed: IndexSet, inserted: IndexSet, updated: IndexSet, moved: Set) { + self.removed = removed + self.inserted = inserted + self.updated = updated + self.moved = moved + } + + /// Whether applying the diff changes nothing. + public var isEmpty: Bool { removed.isEmpty && inserted.isEmpty && updated.isEmpty } + + /// The number of row insertions and removals, a proxy for AppKit work. + public var structuralChangeCount: Int { removed.count + inserted.count } + + /// Computes the diff between two child lists, matching rows by path. + /// + /// Runs in `O(n log n)`: a path index, then a longest increasing + /// subsequence over surviving rows to find the fewest moves. Sort-order + /// changes therefore cost moves only for rows that actually change + /// relative position. + /// - Parameters: + /// - old: The currently displayed children. + /// - new: The children to display. + public init(old: [FileTreeEntry], new: [FileTreeEntry]) { + if old == new { + self = .empty + return + } + if old.isEmpty { + self.init(removed: [], inserted: IndexSet(integersIn: 0..() + + // Surviving rows in new order, with their old positions. + var survivorNewIndices: [Int] = [] + var survivorOldIndices: [Int] = [] + survivorNewIndices.reserveCapacity(min(old.count, new.count)) + survivorOldIndices.reserveCapacity(min(old.count, new.count)) + var survivingOld = IndexSet() + for (newIndex, entry) in new.enumerated() { + if let oldIndex = oldIndexByPath[entry.path] { + survivorNewIndices.append(newIndex) + survivorOldIndices.append(oldIndex) + survivingOld.insert(oldIndex) + } else { + inserted.insert(newIndex) + } + } + removed = IndexSet(integersIn: 0.. Set { + guard !values.isEmpty else { return [] } + // Fast path: already increasing, the common case for plain inserts and removals. + var isIncreasing = true + for index in 1.. 0 { predecessor[position] = tailPositions[low - 1] } + if low == tailPositions.count { + tailPositions.append(position) + } else { + tailPositions[low] = position + } + } + var result = Set() + var cursor = tailPositions.last ?? -1 + while cursor >= 0 { + result.insert(cursor) + cursor = predecessor[cursor] + } + return result + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeDirectoryUpdate.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeDirectoryUpdate.swift new file mode 100644 index 000000000000..4d2a75dc237c --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeDirectoryUpdate.swift @@ -0,0 +1,33 @@ +/// The outcome of loading or refreshing one directory. +public struct FileTreeDirectoryUpdate: Sendable, Equatable { + /// What happened to the directory. + public enum Outcome: Sendable, Equatable { + /// The directory listed successfully. + /// - Parameters: + /// - entries: The visible children in display order. + /// - diff: The change from the previously delivered children. + /// - omittedCount: Entries the provider left out; zero when complete. + case loaded(entries: [FileTreeEntry], diff: FileTreeChildrenDiff, omittedCount: Int) + /// The directory could not be listed. Previously delivered children stay. + /// - Parameter message: A localized, user-presentable reason. + case failed(message: String) + } + + /// The directory path. + public let path: String + /// Whether this is the first successful delivery for the path. + public let isInitialLoad: Bool + /// The result. + public let outcome: Outcome + + /// Creates an update. + /// - Parameters: + /// - path: The directory path. + /// - isInitialLoad: Whether no children were delivered before. + /// - outcome: The result. + public init(path: String, isInitialLoad: Bool, outcome: Outcome) { + self.path = path + self.isInitialLoad = isInitialLoad + self.outcome = outcome + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeEngine.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeEngine.swift new file mode 100644 index 000000000000..fe1c5194326a --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeEngine.swift @@ -0,0 +1,163 @@ +/// Loads, caches, sorts and diffs directory listings off the main actor. +/// +/// One engine serves one root on one provider. The main actor owns row +/// objects and expansion; the engine owns raw listings and the last visible +/// child list it delivered per directory, so every delivery carries an +/// incremental ``FileTreeChildrenDiff`` instead of a reload. Toggling hidden +/// files or the sort order re-derives cached listings without I/O. +/// +/// ```swift +/// let engine = FileTreeEngine(provider: LocalFileTreeProvider()) +/// let updates = await engine.load(["/Users/me/project"]) +/// ``` +public actor FileTreeEngine { + private let provider: any FileTreeProvider + private var sortOrder: FileTreeSortOrder + private var showsHiddenFiles: Bool + /// Raw listings as the provider returned them, by directory path. + private var listings: [String: FileTreeListing] = [:] + /// The children last delivered per directory, in display order. + private var delivered: [String: [FileTreeEntry]] = [:] + /// Bumped when a load for the path starts; a finishing load whose number + /// is no longer current lost a race and is dropped. + private var generations: [String: UInt64] = [:] + + /// Creates an engine. + /// - Parameters: + /// - provider: The filesystem to browse. + /// - sortOrder: The initial sibling order. + /// - showsHiddenFiles: Whether hidden entries are delivered. + public init( + provider: any FileTreeProvider, + sortOrder: FileTreeSortOrder = .standard, + showsHiddenFiles: Bool = true + ) { + self.provider = provider + self.sortOrder = sortOrder + self.showsHiddenFiles = showsHiddenFiles + } + + /// Lists directories through the provider's batch call and returns one + /// update per path that finished without being superseded. + /// + /// Loading an already-loaded path re-lists it and delivers only the diff. + /// - Parameter paths: Absolute directory paths. + /// - Returns: Updates in the order of `paths`. + public func load(_ paths: [String]) async -> [FileTreeDirectoryUpdate] { + var unique: [String] = [] + var seen = Set() + for path in paths where seen.insert(path).inserted { + unique.append(path) + } + guard !unique.isEmpty else { return [] } + var tickets: [String: UInt64] = [:] + for path in unique { + let next = (generations[path] ?? 0) &+ 1 + generations[path] = next + tickets[path] = next + } + let results: [String: Result] + if unique.count == 1, let path = unique.first { + do { + results = [path: .success(try await provider.listDirectory(at: path))] + } catch { + results = [path: .failure(error)] + } + } else { + results = await provider.listDirectories(at: unique) + } + if Task.isCancelled { return [] } + var updates: [FileTreeDirectoryUpdate] = [] + for path in unique { + guard generations[path] == tickets[path], let result = results[path] else { continue } + switch result { + case .success(let listing): + listings[path] = listing + updates.append(deliver(path: path)) + case .failure(let error): + if error is CancellationError { continue } + updates.append(FileTreeDirectoryUpdate( + path: path, + isInitialLoad: delivered[path] == nil, + outcome: .failed(message: error.localizedDescription) + )) + } + } + return updates + } + + /// Changes sorting or hidden-file visibility and re-derives every cached + /// directory without I/O. + /// - Parameters: + /// - sortOrder: The new sibling order. + /// - showsHiddenFiles: Whether hidden entries are delivered. + /// - Returns: Updates for directories whose visible children changed. + public func setPresentation(sortOrder: FileTreeSortOrder, showsHiddenFiles: Bool) -> [FileTreeDirectoryUpdate] { + guard sortOrder != self.sortOrder || showsHiddenFiles != self.showsHiddenFiles else { return [] } + self.sortOrder = sortOrder + self.showsHiddenFiles = showsHiddenFiles + return listings.keys.sorted().compactMap { path in + let update = deliver(path: path) + if case .loaded(_, let diff, _) = update.outcome, diff.isEmpty { return nil } + return update + } + } + + /// Forgets cached listings at and below `path`, so the next load of any of + /// them is an initial load. Call when rows under `path` are discarded. + /// - Parameter path: The directory whose subtree to forget. + public func discard(subtreeAt path: String) { + let prefix = path == "/" ? "/" : path + "/" + for key in Array(listings.keys) where key == path || key.hasPrefix(prefix) { + listings[key] = nil + delivered[key] = nil + generations[key] = (generations[key] ?? 0) &+ 1 + } + } + + /// The directories with a cached listing. + public func loadedDirectories() -> Set { + Set(listings.keys) + } + + /// The cached directories a change batch touches. + /// + /// Changes inside directories that were never loaded cost only a lookup. + /// - Parameter batch: A batch from ``FileTreeProvider/changes(under:)``. + /// - Returns: Loaded directories to refresh. + public func affectedDirectories(for batch: FileTreeChangeBatch) -> Set { + var result = Set() + for directory in batch.directories where listings[directory] != nil { + result.insert(directory) + } + if !batch.subtrees.isEmpty { + for key in listings.keys { + for subtree in batch.subtrees { + let prefix = subtree == "/" ? "/" : subtree + "/" + if key == subtree || key.hasPrefix(prefix) { + result.insert(key) + break + } + } + } + } + return result + } + + private func deliver(path: String) -> FileTreeDirectoryUpdate { + let listing = listings[path] ?? FileTreeListing(entries: []) + let visibleEntries = showsHiddenFiles ? listing.entries : listing.entries.filter { !$0.isHidden } + let next = sortOrder.sorted(visibleEntries) + let previous = delivered[path] + delivered[path] = next + return FileTreeDirectoryUpdate( + path: path, + isInitialLoad: previous == nil, + outcome: .loaded( + entries: next, + diff: FileTreeChildrenDiff(old: previous ?? [], new: next), + omittedCount: listing.omittedCount + ) + ) + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeEntry.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeEntry.swift new file mode 100644 index 000000000000..51caab24bd23 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeEntry.swift @@ -0,0 +1,82 @@ +public import Foundation + +/// One directory entry as a file tree provider reports it. +/// +/// Entries are plain values so a listing can move between the provider, the +/// ``FileTreeEngine`` actor and the main actor without copying object graphs. +/// The absolute ``path`` is the stable identity of an entry: two listings that +/// report the same path describe the same row, and ``FileTreeChildrenDiff`` +/// matches rows by it. +public struct FileTreeEntry: Sendable, Hashable { + /// The last path component shown in the tree. + public let name: String + /// The absolute path on the provider's filesystem. Stable row identity. + public let path: String + /// The filesystem object kind. + public let kind: FileTreeEntryKind + /// The byte size for regular files, or `nil` when unknown or not a file. + public let size: Int64? + /// The modification time in seconds since 1970, or `nil` when unknown. + public let modificationTime: TimeInterval? + /// Whether the filesystem marks the entry hidden (the `UF_HIDDEN` flag). + /// + /// Dot-prefixed names are hidden regardless; see ``isHidden``. + public let hasHiddenFlag: Bool + + /// Creates an entry. + /// - Parameters: + /// - name: The last path component. + /// - path: The absolute path, used as the row identity. + /// - kind: The object kind. + /// - size: The byte size for regular files. + /// - modificationTime: Seconds since 1970, when the provider knows it. + /// - hasHiddenFlag: Whether the filesystem hides the entry with `UF_HIDDEN`. + public init( + name: String, + path: String, + kind: FileTreeEntryKind, + size: Int64? = nil, + modificationTime: TimeInterval? = nil, + hasHiddenFlag: Bool = false + ) { + self.name = name + self.path = path + self.kind = kind + self.size = size + self.modificationTime = modificationTime + self.hasHiddenFlag = hasHiddenFlag + } + + /// Whether the row can be expanded: a directory or a symlink to one. + public var isDirectory: Bool { + switch kind { + case .directory, .symbolicLinkToDirectory: return true + case .file, .symbolicLink, .other: return false + } + } + + /// Whether the entry is hidden unless the viewer shows hidden files. + public var isHidden: Bool { + hasHiddenFlag || name.hasPrefix(".") + } + + /// The lowercase path extension, or an empty string. + public var pathExtension: String { + guard let dot = name.lastIndex(of: "."), dot != name.startIndex else { return "" } + return name[name.index(after: dot)...].lowercased() + } +} + +/// The filesystem object kind of a ``FileTreeEntry``. +public enum FileTreeEntryKind: Sendable, Hashable { + /// A directory. Expandable. + case directory + /// A regular file. + case file + /// A symbolic link whose target is a directory. Expandable, like Finder aliases. + case symbolicLinkToDirectory + /// A symbolic link to a file, or a dangling link. + case symbolicLink + /// A socket, FIFO, device or other special file. + case other +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeListing.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeListing.swift new file mode 100644 index 000000000000..da545bd50e55 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeListing.swift @@ -0,0 +1,22 @@ +/// The complete, unfiltered contents of one directory. +/// +/// Providers return every entry, hidden ones included, so toggling hidden +/// files re-filters cached listings instead of repeating I/O. +public struct FileTreeListing: Sendable, Equatable { + /// The entries in provider order. The engine sorts them. + public var entries: [FileTreeEntry] + /// The number of entries the provider omitted to stay within its limit. + /// + /// Zero for a complete listing. Remote providers cap very large directories + /// so one listing cannot exhaust a transport's output limit. + public var omittedCount: Int + + /// Creates a listing. + /// - Parameters: + /// - entries: The entries in any order. + /// - omittedCount: How many entries the provider left out; zero when complete. + public init(entries: [FileTreeEntry], omittedCount: Int = 0) { + self.entries = entries + self.omittedCount = omittedCount + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeNameCollationKey.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeNameCollationKey.swift new file mode 100644 index 000000000000..1892783f3966 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeNameCollationKey.swift @@ -0,0 +1,81 @@ +/// A precomputed, Finder-like sort key for a file name. +/// +/// Comparison is case-insensitive and numeric-aware, so `file9` sorts before +/// `file10`, like `localizedStandardCompare`. Building the key once per entry +/// and comparing bytes is what keeps sorting a 50,000-entry directory to tens +/// of milliseconds; `localizedStandardCompare` inside the sort comparator costs +/// hundreds. +struct FileTreeNameCollationKey: Sendable { + private let folded: ContiguousArray + private let original: ContiguousArray + + /// Creates the key for one file name. + /// - Parameter name: The displayed file name. + init(_ name: String) { + folded = ContiguousArray(name.lowercased().utf8) + original = ContiguousArray(name.utf8) + } + + /// Orders two keys: folded natural order first, raw bytes as the tie-break + /// so the order is total and stable across runs. + /// - Returns: A negative value, zero or a positive value. + func compare(_ other: FileTreeNameCollationKey) -> Int { + let primary = Self.naturalCompare(folded, other.folded) + if primary != 0 { return primary } + return Self.byteCompare(original, other.original) + } + + private static func isDigit(_ byte: UInt8) -> Bool { byte >= 48 && byte <= 57 } + + private static func naturalCompare(_ lhs: ContiguousArray, _ rhs: ContiguousArray) -> Int { + var i = 0 + var j = 0 + let lhsCount = lhs.count + let rhsCount = rhs.count + while i < lhsCount && j < rhsCount { + let a = lhs[i] + let b = rhs[j] + if isDigit(a) && isDigit(b) { + // Compare the digit runs as numbers: skip leading zeros, then + // a longer run is larger, then compare digit by digit. + var iStart = i + var jStart = j + while iStart < lhsCount && lhs[iStart] == 48 { iStart += 1 } + while jStart < rhsCount && rhs[jStart] == 48 { jStart += 1 } + var iEnd = iStart + var jEnd = jStart + while iEnd < lhsCount && isDigit(lhs[iEnd]) { iEnd += 1 } + while jEnd < rhsCount && isDigit(rhs[jEnd]) { jEnd += 1 } + let lengthDelta = (iEnd - iStart) - (jEnd - jStart) + if lengthDelta != 0 { return lengthDelta } + var p = iStart + var q = jStart + while p < iEnd { + if lhs[p] != rhs[q] { return Int(lhs[p]) - Int(rhs[q]) } + p += 1 + q += 1 + } + // Equal values: fewer leading zeros first ("1" before "01"). + let zeroDelta = (iStart - i) - (jStart - j) + if zeroDelta != 0 { return zeroDelta } + i = iEnd + j = jEnd + continue + } + if a != b { return Int(a) - Int(b) } + i += 1 + j += 1 + } + return (lhsCount - i) - (rhsCount - j) + } + + private static func byteCompare(_ lhs: ContiguousArray, _ rhs: ContiguousArray) -> Int { + let count = min(lhs.count, rhs.count) + var index = 0 + while index < count { + if lhs[index] != rhs[index] { return Int(lhs[index]) - Int(rhs[index]) } + index += 1 + } + return lhs.count - rhs.count + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeProvider.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeProvider.swift new file mode 100644 index 000000000000..81d1df490db9 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeProvider.swift @@ -0,0 +1,54 @@ +/// A filesystem the file tree can browse: this Mac, an SSH host or a Cloud VM. +/// +/// Conformers perform I/O off the main actor and must be safe to call +/// concurrently. ``FileTreeEngine`` is the only caller in the app. +/// +/// ```swift +/// struct FixtureProvider: FileTreeProvider { +/// func listDirectory(at path: String) async throws -> FileTreeListing { +/// FileTreeListing(entries: [FileTreeEntry(name: "a", path: path + "/a", kind: .file)]) +/// } +/// } +/// ``` +public protocol FileTreeProvider: Sendable { + /// Lists one directory, hidden entries included. + /// - Parameter path: The absolute directory path. + /// - Returns: The directory's entries in any order. + /// - Throws: A provider error when the directory cannot be read. + func listDirectory(at path: String) async throws -> FileTreeListing + + /// Lists several directories, ideally in one round trip. + /// + /// Restoring an expanded tree over SSH would otherwise pay one network + /// round trip per directory. The default implementation lists the paths + /// one at a time. + /// - Parameter paths: Absolute directory paths, without duplicates. + /// - Returns: One result per requested path, keyed by path. + func listDirectories(at paths: [String]) async -> [String: Result] + + /// A stream of filesystem change batches under `rootPath`, or `nil` when + /// the provider cannot observe changes and relies on explicit refreshes. + /// - Parameter rootPath: The absolute root the tree displays. + func changes(under rootPath: String) -> AsyncStream? +} + +extension FileTreeProvider { + /// Lists each path in turn. + public func listDirectories(at paths: [String]) async -> [String: Result] { + var results: [String: Result] = [:] + for path in paths { + if Task.isCancelled { break } + do { + results[path] = .success(try await listDirectory(at: path)) + } catch { + results[path] = .failure(error) + } + } + return results + } + + /// No change observation by default. + public func changes(under rootPath: String) -> AsyncStream? { + nil + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeSortOrder.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeSortOrder.swift new file mode 100644 index 000000000000..a1ed34dc00d1 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeSortOrder.swift @@ -0,0 +1,86 @@ +/// How the tree orders siblings, like Finder's View Options. +public struct FileTreeSortOrder: Sendable, Hashable, Codable { + /// The attribute siblings are ordered by. + public enum Key: String, Sendable, Hashable, Codable, CaseIterable { + /// Finder-like natural name order. + case name + /// File extension, then name. + case kind + /// Modification time, then name. + case dateModified + /// File size (directories count as zero), then name. + case size + } + + /// The primary attribute. + public var key: Key + /// Whether the primary attribute ascends. Name ties always ascend. + public var ascending: Bool + /// Whether directories stay above files regardless of the key. + public var foldersFirst: Bool + + /// Creates a sort order. + /// - Parameters: + /// - key: The primary attribute; name by default. + /// - ascending: Whether it ascends; true by default. + /// - foldersFirst: Whether directories stay on top; true by default, as + /// every editor file tree does. + public init(key: Key = .name, ascending: Bool = true, foldersFirst: Bool = true) { + self.key = key + self.ascending = ascending + self.foldersFirst = foldersFirst + } + + /// The default order: folders first, then names ascending. + public static let standard = FileTreeSortOrder() + + /// Returns `entries` in this order. + /// + /// Sort keys are computed once per entry, so the cost is one pass of key + /// building plus an `O(n log n)` byte-comparison sort. + /// - Parameter entries: Entries in any order. + /// - Returns: The sorted entries. + public func sorted(_ entries: [FileTreeEntry]) -> [FileTreeEntry] { + guard entries.count > 1 else { return entries } + let keys = entries.map { FileTreeNameCollationKey($0.name) } + let extensions: [String] = key == .kind ? entries.map(\.pathExtension) : [] + var indices = Array(entries.indices) + indices.sort { lhs, rhs in + compare(lhs, rhs, entries: entries, keys: keys, extensions: extensions) < 0 + } + return indices.map { entries[$0] } + } + + private func compare( + _ lhs: Int, + _ rhs: Int, + entries: [FileTreeEntry], + keys: [FileTreeNameCollationKey], + extensions: [String] + ) -> Int { + let a = entries[lhs] + let b = entries[rhs] + if foldersFirst, a.isDirectory != b.isDirectory { + return a.isDirectory ? -1 : 1 + } + let nameOrder = keys[lhs].compare(keys[rhs]) + var primary = 0 + switch key { + case .name: + primary = nameOrder + case .kind: + primary = extensions[lhs] == extensions[rhs] ? 0 : (extensions[lhs] < extensions[rhs] ? -1 : 1) + case .dateModified: + let x = a.modificationTime ?? 0 + let y = b.modificationTime ?? 0 + primary = x == y ? 0 : (x < y ? -1 : 1) + case .size: + let x = a.isDirectory ? 0 : (a.size ?? 0) + let y = b.isDirectory ? 0 : (b.size ?? 0) + primary = x == y ? 0 : (x < y ? -1 : 1) + } + if primary != 0 { return ascending ? primary : -primary } + if key == .name { return 0 } + return nameOrder + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeViewState.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeViewState.swift new file mode 100644 index 000000000000..b067f08f91b6 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeViewState.swift @@ -0,0 +1,52 @@ +/// What a viewer had open in one tree: expansion, selection and scroll. +/// +/// Paths are relative to the tree root so a state is small and survives +/// the root being displayed through a different absolute prefix. +public struct FileTreeViewState: Codable, Sendable, Equatable { + /// Expanded directories, relative to the root. + public var expandedPaths: [String] + /// Selected rows, relative to the root. + public var selectedPaths: [String] + /// The keyboard anchor among ``selectedPaths``. + public var anchorPath: String? + /// The row at the top of the viewport, relative to the root. + public var topVisiblePath: String? + /// How far the viewport starts below the top of ``topVisiblePath``, in points. + public var topVisibleOffset: Double + + /// Creates a view state. + /// - Parameters: + /// - expandedPaths: Expanded directories relative to the root. + /// - selectedPaths: Selected rows relative to the root. + /// - anchorPath: The keyboard anchor row. + /// - topVisiblePath: The first visible row. + /// - topVisibleOffset: Points between that row's top and the viewport top. + public init( + expandedPaths: [String] = [], + selectedPaths: [String] = [], + anchorPath: String? = nil, + topVisiblePath: String? = nil, + topVisibleOffset: Double = 0 + ) { + self.expandedPaths = expandedPaths + self.selectedPaths = selectedPaths + self.anchorPath = anchorPath + self.topVisiblePath = topVisiblePath + self.topVisibleOffset = topVisibleOffset + } + + /// Converts an absolute path under `root` to the stored relative form. + /// - Returns: `""` for the root itself, `nil` for paths outside it. + public static func relativePath(_ path: String, root: String) -> String? { + if path == root { return "" } + let prefix = root == "/" ? "/" : root + "/" + guard path.hasPrefix(prefix) else { return nil } + return String(path.dropFirst(prefix.count)) + } + + /// Converts a stored relative path back to an absolute path under `root`. + public static func absolutePath(_ relative: String, root: String) -> String { + if relative.isEmpty { return root } + return root == "/" ? "/" + relative : root + "/" + relative + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeViewStateRepository.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeViewStateRepository.swift new file mode 100644 index 000000000000..6ab461793936 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/FileTreeViewStateRepository.swift @@ -0,0 +1,79 @@ +public import Foundation + +/// Persists ``FileTreeViewState`` per tree scope in `UserDefaults`. +/// +/// A scope names one workspace's view of one root (for example +/// `"|local:/Users/me/project"`). The repository keeps the +/// most recently used ``capacity`` scopes and caps each state's path lists so +/// the defaults blob stays small. +public actor FileTreeViewStateRepository { + private struct Stored: Codable { + var order: [String] = [] + var states: [String: FileTreeViewState] = [:] + } + + private let defaults: UserDefaults + private let key: String + private let capacity: Int + private let maxPathsPerState: Int + private var stored: Stored? + + /// Creates a repository. + /// - Parameters: + /// - defaults: The store; tests pass a suite-scoped instance. + /// - key: The defaults key holding every state. + /// - capacity: How many scopes to remember, least recently used dropped first. + /// - maxPathsPerState: The cap on expanded and selected paths per scope. + public init( + defaults: UserDefaults, + key: String = "fileExplorer.viewStates.v1", + capacity: Int = 64, + maxPathsPerState: Int = 2_000 + ) { + self.defaults = defaults + self.key = key + self.capacity = capacity + self.maxPathsPerState = maxPathsPerState + } + + /// The saved state for `scope`, if any. + public func state(for scope: String) -> FileTreeViewState? { + load().states[scope] + } + + /// Saves or clears the state for `scope` and marks it most recently used. + /// - Parameters: + /// - state: The state, or `nil` to forget the scope. + /// - scope: The scope key. + public func save(_ state: FileTreeViewState?, for scope: String) { + var current = load() + current.order.removeAll { $0 == scope } + if var state { + state.expandedPaths = Array(state.expandedPaths.prefix(maxPathsPerState)) + state.selectedPaths = Array(state.selectedPaths.prefix(maxPathsPerState)) + guard current.states[scope] != state || current.order.last != scope else { + stored = current + return + } + current.states[scope] = state + current.order.append(scope) + while current.order.count > capacity { + let evicted = current.order.removeFirst() + current.states[evicted] = nil + } + } else { + current.states[scope] = nil + } + stored = current + if let data = try? JSONEncoder().encode(current) { + defaults.set(data, forKey: key) + } + } + + private func load() -> Stored { + if let stored { return stored } + let decoded = defaults.data(forKey: key).flatMap { try? JSONDecoder().decode(Stored.self, from: $0) } ?? Stored() + stored = decoded + return decoded + } +} diff --git a/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/LocalFileTreeProvider.swift b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/LocalFileTreeProvider.swift new file mode 100644 index 000000000000..166f67d07615 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Sources/CmuxFileTree/LocalFileTreeProvider.swift @@ -0,0 +1,26 @@ +/// Browses this Mac's filesystem. +/// +/// Listing uses `getattrlistbulk(2)` on a background executor; change +/// observation uses one FSEvents stream per root. +public struct LocalFileTreeProvider: FileTreeProvider { + private let reader: DirectoryBulkReader + private let eventLatency: Double + + /// Creates a local provider. + /// - Parameter eventLatency: FSEvents coalescing window in seconds. + public init(eventLatency: Double = 0.25) { + self.reader = DirectoryBulkReader() + self.eventLatency = eventLatency + } + + /// Lists one directory without following it into the main actor. + public func listDirectory(at path: String) async throws -> FileTreeListing { + try Task.checkCancellation() + return FileTreeListing(entries: try reader.entries(atPath: path)) + } + + /// Streams FSEvents batches under `rootPath`. + public func changes(under rootPath: String) -> AsyncStream? { + FileSystemEventStream(rootPath: rootPath, latency: eventLatency).makeStream() + } +} diff --git a/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeChildrenDiffTests.swift b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeChildrenDiffTests.swift new file mode 100644 index 000000000000..5285c9c877a7 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeChildrenDiffTests.swift @@ -0,0 +1,85 @@ +import Foundation +import Testing +@testable import CmuxFileTree + +/// Applies a diff the way `NSOutlineView` batch updates do: removals in old +/// coordinates, then insertions in new coordinates, in ascending order. +private func apply(_ diff: FileTreeChildrenDiff, old: [FileTreeEntry], new: [FileTreeEntry]) -> [FileTreeEntry] { + var rows = old + for index in diff.removed.reversed() { + rows.remove(at: index) + } + for index in diff.inserted { + rows.insert(new[index], at: index) + } + for index in diff.updated { + rows[index] = new[index] + } + return rows +} + +private func entry(_ name: String, size: Int64 = 0, directory: Bool = false) -> FileTreeEntry { + FileTreeEntry(name: name, path: "/root/" + name, kind: directory ? .directory : .file, size: size) +} + +@Suite struct FileTreeChildrenDiffTests { + @Test func identicalListsProduceNoChanges() { + let rows = (0..<10).map { entry("f\($0)") } + #expect(FileTreeChildrenDiff(old: rows, new: rows).isEmpty) + } + + @Test func insertAndRemoveKeepSurvivorsInPlace() { + let old = ["a", "b", "c", "d"].map { entry($0) } + let new = ["a", "c", "d", "e"].map { entry($0) } + let diff = FileTreeChildrenDiff(old: old, new: new) + #expect(diff.removed == IndexSet(integer: 1)) + #expect(diff.inserted == IndexSet(integer: 3)) + #expect(diff.moved.isEmpty) + #expect(apply(diff, old: old, new: new) == new) + } + + @Test func metadataChangeIsAnUpdateNotAMove() { + let old = [entry("a", size: 1), entry("b", size: 1)] + let new = [entry("a", size: 1), entry("b", size: 2)] + let diff = FileTreeChildrenDiff(old: old, new: new) + #expect(diff.structuralChangeCount == 0) + #expect(diff.updated == IndexSet(integer: 1)) + #expect(apply(diff, old: old, new: new) == new) + } + + @Test func reorderMovesTheFewestRows() { + let old = ["a", "b", "c", "d", "e"].map { entry($0) } + let new = ["b", "c", "d", "e", "a"].map { entry($0) } + let diff = FileTreeChildrenDiff(old: old, new: new) + #expect(diff.moved == ["/root/a"]) + #expect(diff.structuralChangeCount == 2) + #expect(apply(diff, old: old, new: new) == new) + } + + @Test(arguments: 0..<200) + func randomEditsRoundTrip(seed: Int) { + var generator = SeededGenerator(seed: UInt64(seed)) + let universe = (0..<40).map { entry("n\($0)", size: Int64($0)) } + let old = universe.filter { _ in Bool.random(using: &generator) }.shuffled(using: &generator) + var new = universe.filter { _ in Bool.random(using: &generator) }.shuffled(using: &generator) + if !new.isEmpty, Bool.random(using: &generator) { + let index = Int.random(in: 0.. UInt64 { + state &+= 0x9E37_79B9_7F4A_7C15 + var z = state + z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 + z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB + return z ^ (z >> 31) + } +} diff --git a/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeEngineTests.swift b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeEngineTests.swift new file mode 100644 index 000000000000..c139c5bf454c --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeEngineTests.swift @@ -0,0 +1,165 @@ +import Foundation +import Testing +@testable import CmuxFileTree + +/// A provider whose listings and gates are scripted by the test. +private actor ScriptedFileSystem { + var listings: [String: [FileTreeEntry]] = [:] + var listCalls: [[String]] = [] + var failures: [String: String] = [:] + private var gates: [String: CheckedContinuation] = [:] + private var gatedPaths: Set = [] + + func set(_ path: String, _ entries: [FileTreeEntry]) { listings[path] = entries } + func fail(_ path: String, _ message: String) { failures[path] = message } + func gate(_ path: String) { gatedPaths.insert(path) } + func release(_ path: String) { + gates.removeValue(forKey: path)?.resume() + } + func isWaiting(_ path: String) -> Bool { gates[path] != nil } + + func list(_ paths: [String]) async -> [String: Result] { + listCalls.append(paths) + var results: [String: Result] = [:] + // Snapshot before any gate so a gated call returns what it saw first. + let snapshot = listings + for path in paths { + if gatedPaths.remove(path) != nil { + await withCheckedContinuation { gates[path] = $0 } + } + if let message = failures[path] { + results[path] = .failure(ScriptedError(message: message)) + } else { + results[path] = .success(FileTreeListing(entries: snapshot[path] ?? [])) + } + } + return results + } +} + +private struct ScriptedError: LocalizedError { + let message: String + var errorDescription: String? { message } +} + +private struct ScriptedProvider: FileTreeProvider { + let fileSystem: ScriptedFileSystem + func listDirectory(at path: String) async throws -> FileTreeListing { + try await fileSystem.list([path])[path]!.get() + } + func listDirectories(at paths: [String]) async -> [String: Result] { + await fileSystem.list(paths) + } +} + +private func file(_ parent: String, _ name: String) -> FileTreeEntry { + FileTreeEntry(name: name, path: parent + "/" + name, kind: .file) +} + +private func folder(_ parent: String, _ name: String) -> FileTreeEntry { + FileTreeEntry(name: name, path: parent + "/" + name, kind: .directory) +} + +private func loadedNames(_ update: FileTreeDirectoryUpdate?) -> [String]? { + guard case .loaded(let entries, _, _)? = update?.outcome else { return nil } + return entries.map(\.name) +} + +@Suite struct FileTreeEngineTests { + @Test func firstLoadInsertsEverythingAndRefreshDeliversOnlyTheDiff() async { + let fs = ScriptedFileSystem() + await fs.set("/r", [file("/r", "b"), folder("/r", "src"), file("/r", "a")]) + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs)) + + let first = await engine.load(["/r"]).first + #expect(first?.isInitialLoad == true) + #expect(loadedNames(first) == ["src", "a", "b"]) + + await fs.set("/r", [file("/r", "b"), folder("/r", "src"), file("/r", "a"), file("/r", "c")]) + let second = await engine.load(["/r"]).first + #expect(second?.isInitialLoad == false) + guard case .loaded(_, let diff, _)? = second?.outcome else { + Issue.record("expected a loaded update") + return + } + #expect(diff.removed.isEmpty) + #expect(diff.inserted == IndexSet(integer: 3)) + } + + @Test func batchLoadsUseOneProviderCall() async { + let fs = ScriptedFileSystem() + await fs.set("/r/a", [file("/r/a", "1")]) + await fs.set("/r/b", [file("/r/b", "2")]) + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs)) + let updates = await engine.load(["/r/a", "/r/b", "/r/a"]) + #expect(updates.map(\.path) == ["/r/a", "/r/b"]) + #expect(await fs.listCalls == [["/r/a", "/r/b"]]) + } + + @Test func hiddenToggleAndSortChangeNeedNoIO() async { + let fs = ScriptedFileSystem() + await fs.set("/r", [file("/r", ".env"), file("/r", "b"), file("/r", "a")]) + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs), showsHiddenFiles: false) + #expect(loadedNames(await engine.load(["/r"]).first) == ["a", "b"]) + + let shown = await engine.setPresentation(sortOrder: .standard, showsHiddenFiles: true) + #expect(loadedNames(shown.first) == [".env", "a", "b"]) + let reversed = await engine.setPresentation( + sortOrder: FileTreeSortOrder(ascending: false), + showsHiddenFiles: true + ) + #expect(loadedNames(reversed.first) == ["b", "a", ".env"]) + #expect(await fs.listCalls.count == 1) + } + + @Test func supersededLoadIsDropped() async { + let fs = ScriptedFileSystem() + await fs.set("/r", [file("/r", "old")]) + await fs.gate("/r") + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs)) + let slow = Task { await engine.load(["/r"]) } + while await !fs.isWaiting("/r") { await Task.yield() } + // A newer load starts and finishes while the first one is suspended. + await fs.set("/r", [file("/r", "new")]) + let fresh = await engine.load(["/r"]) + await fs.release("/r") + let stale = await slow.value + #expect(loadedNames(fresh.first) == ["new"]) + #expect(stale.isEmpty, "a superseded listing must not overwrite the newer one") + } + + @Test func failureKeepsPreviousChildren() async { + let fs = ScriptedFileSystem() + await fs.set("/r", [file("/r", "a")]) + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs)) + _ = await engine.load(["/r"]) + await fs.fail("/r", "gone") + let failed = await engine.load(["/r"]).first + #expect(failed?.outcome == .failed(message: "gone")) + await fs.fail("/r", "") + let fsFailures = await fs.failures + #expect(fsFailures["/r"] == "") + } + + @Test func changeBatchesOnlyTouchLoadedDirectories() async { + let fs = ScriptedFileSystem() + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs)) + _ = await engine.load(["/r", "/r/src", "/r/src/deep"]) + let direct = await engine.affectedDirectories(for: FileTreeChangeBatch( + directories: ["/r/src", "/r/node_modules/x", "/r/.build/debug"] + )) + #expect(direct == ["/r/src"]) + let subtree = await engine.affectedDirectories(for: FileTreeChangeBatch(subtrees: ["/r/src"])) + #expect(subtree == ["/r/src", "/r/src/deep"]) + } + + @Test func discardMakesTheNextLoadInitial() async { + let fs = ScriptedFileSystem() + await fs.set("/r/a", [file("/r/a", "x")]) + let engine = FileTreeEngine(provider: ScriptedProvider(fileSystem: fs)) + _ = await engine.load(["/r/a"]) + await engine.discard(subtreeAt: "/r") + #expect(await engine.loadedDirectories().isEmpty) + #expect(await engine.load(["/r/a"]).first?.isInitialLoad == true) + } +} diff --git a/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreePerformanceBenchmarkTests.swift b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreePerformanceBenchmarkTests.swift new file mode 100644 index 000000000000..a1e321c85ec0 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreePerformanceBenchmarkTests.swift @@ -0,0 +1,113 @@ +import Foundation +import Testing +@testable import CmuxFileTree + +/// Synthetic 50,000-entry benchmarks for the file tree hot paths. +/// +/// Numbers print to the test log for the PR record. The limits are an order of +/// magnitude above measured values on an M-series Mac so the suite stays +/// stable on loaded CI hosts while still failing on a quadratic regression +/// (the pre-rewrite tree took over 90 s to expand 4,000 children). +@Suite(.serialized) struct FileTreePerformanceBenchmarkTests { + private static let entryCount = 50_000 + + private static func syntheticEntries(parent: String = "/bench") -> [FileTreeEntry] { + (0.. Void) -> Duration { + let clock = ContinuousClock() + let elapsed = clock.measure(body) + print("[file-tree-bench] \(label): \(elapsed.formatted(.units(allowed: [.milliseconds], fractionalPart: .show(length: 1))))") + return elapsed + } + + @Test func sortDiffAndPresentationOnFiftyThousandEntries() async { + let entries = Self.syntheticEntries() + var sorted: [FileTreeEntry] = [] + let sortTime = Self.measure("sort 50k by name") { + sorted = FileTreeSortOrder.standard.sorted(entries) + } + #expect(sorted.count == Self.entryCount) + #expect(sortTime < .seconds(3)) + + let initialDiffTime = Self.measure("initial diff 50k") { + _ = FileTreeChildrenDiff(old: [], new: sorted) + } + #expect(initialDiffTime < .seconds(1)) + + var changed = sorted + changed.remove(at: 25_000) + changed.insert(FileTreeEntry(name: "file-new.js", path: "/bench/file-new.js", kind: .file), at: 40_000) + var diff = FileTreeChildrenDiff.empty + let churnDiffTime = Self.measure("one-file churn diff 50k") { + diff = FileTreeChildrenDiff(old: sorted, new: changed) + } + #expect(diff.structuralChangeCount == 2) + #expect(churnDiffTime < .seconds(2)) + + let resorted = FileTreeSortOrder(key: .size, ascending: false).sorted(entries) + let reorderTime = Self.measure("full reorder diff 50k") { + _ = FileTreeChildrenDiff(old: sorted, new: resorted) + } + #expect(reorderTime < .seconds(3)) + } + + @Test func listAndLoadAFiftyThousandEntryDirectory() async throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-file-tree-bench-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: root) } + for index in 0..= 0 { close(fd) } + } + + let provider = LocalFileTreeProvider() + let clock = ContinuousClock() + var listing = FileTreeListing(entries: []) + let listTime = try await clock.measure { + listing = try await provider.listDirectory(at: root.path) + } + print("[file-tree-bench] getattrlistbulk list 50k: \(listTime)") + #expect(listing.entries.count == Self.entryCount) + #expect(listTime < .seconds(5)) + + let engine = FileTreeEngine(provider: provider) + var updates: [FileTreeDirectoryUpdate] = [] + let loadTime = await clock.measure { + updates = await engine.load([root.path]) + } + print("[file-tree-bench] engine load (list+sort+diff) 50k: \(loadTime)") + #expect(loadTime < .seconds(8)) + + // A single new file refreshes the directory with a one-row diff. + let fd = open(root.path + "/zz-new.js", O_CREAT | O_WRONLY, 0o644) + if fd >= 0 { close(fd) } + var refresh: [FileTreeDirectoryUpdate] = [] + let refreshTime = await clock.measure { + refresh = await engine.load([root.path]) + } + print("[file-tree-bench] engine refresh after one new file 50k: \(refreshTime)") + guard case .loaded(_, let diff, _)? = refresh.first?.outcome else { + Issue.record("expected a loaded refresh") + return + } + #expect(diff.inserted.count == 1) + #expect(diff.removed.isEmpty) + #expect(updates.count == 1) + #expect(refreshTime < .seconds(8)) + } +} diff --git a/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeSortOrderTests.swift b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeSortOrderTests.swift new file mode 100644 index 000000000000..74d9cd10603d --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeSortOrderTests.swift @@ -0,0 +1,48 @@ +import Foundation +import Testing +@testable import CmuxFileTree + +@Suite struct FileTreeSortOrderTests { + private func names(_ entries: [FileTreeEntry]) -> [String] { entries.map(\.name) } + + private func file(_ name: String, size: Int64 = 0, time: TimeInterval = 0) -> FileTreeEntry { + FileTreeEntry(name: name, path: "/r/" + name, kind: .file, size: size, modificationTime: time) + } + + private func folder(_ name: String) -> FileTreeEntry { + FileTreeEntry(name: name, path: "/r/" + name, kind: .directory) + } + + @Test func nameOrderIsNaturalAndCaseInsensitive() { + let input = ["file10", "File2", "file1", "b", "A", "file02"].map { file($0) } + #expect(names(FileTreeSortOrder.standard.sorted(input)) == ["A", "b", "file1", "File2", "file02", "file10"]) + } + + @Test func nameOrderMatchesFinderStandardCompareForCommonNames() { + let raw = ["README.md", "readme.txt", "src", "Sources", "a10.js", "a9.js", "_private", "Zeta", "alpha", "ALPHA2"] + let expected = raw.sorted { $0.localizedStandardCompare($1) == .orderedAscending } + #expect(names(FileTreeSortOrder(foldersFirst: false).sorted(raw.map { file($0) })) == expected) + } + + @Test func foldersStayFirstUnlessDisabled() { + let input = [file("a"), folder("z"), file("b"), folder("c")] + #expect(names(FileTreeSortOrder.standard.sorted(input)) == ["c", "z", "a", "b"]) + #expect(names(FileTreeSortOrder(foldersFirst: false).sorted(input)) == ["a", "b", "c", "z"]) + } + + @Test func descendingReversesThePrimaryKey() { + let input = [file("a"), file("c"), file("b")] + #expect(names(FileTreeSortOrder(ascending: false).sorted(input)) == ["c", "b", "a"]) + } + + @Test func sizeAndDateKeysBreakTiesByName() { + let input = [file("b", size: 5, time: 2), file("a", size: 5, time: 2), file("c", size: 1, time: 9)] + #expect(names(FileTreeSortOrder(key: .size).sorted(input)) == ["c", "a", "b"]) + #expect(names(FileTreeSortOrder(key: .dateModified, ascending: false).sorted(input)) == ["c", "a", "b"]) + } + + @Test func kindGroupsByExtension() { + let input = [file("b.swift"), file("a.md"), file("c.swift"), file("Makefile")] + #expect(names(FileTreeSortOrder(key: .kind).sorted(input)) == ["Makefile", "a.md", "b.swift", "c.swift"]) + } +} diff --git a/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeViewStateRepositoryTests.swift b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeViewStateRepositoryTests.swift new file mode 100644 index 000000000000..28978607a94b --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/FileTreeViewStateRepositoryTests.swift @@ -0,0 +1,51 @@ +import Foundation +import Testing +@testable import CmuxFileTree + +@Suite struct FileTreeViewStateRepositoryTests { + private func makeDefaults(suite: String = "cmux-file-tree-state-\(UUID().uuidString)") -> UserDefaults { + UserDefaults(suiteName: suite)! + } + + @Test func statesSurviveANewRepositoryInstance() async { + let suite = "cmux-file-tree-state-\(UUID().uuidString)" + defer { UserDefaults().removePersistentDomain(forName: suite) } + let state = FileTreeViewState( + expandedPaths: ["src", "src/app"], + selectedPaths: ["src/app/main.swift"], + anchorPath: "src/app/main.swift", + topVisiblePath: "src", + topVisibleOffset: 4 + ) + await FileTreeViewStateRepository(defaults: makeDefaults(suite: suite)).save(state, for: "ws|local:/p") + let restored = await FileTreeViewStateRepository(defaults: makeDefaults(suite: suite)).state(for: "ws|local:/p") + #expect(restored == state) + } + + @Test func leastRecentlyUsedScopesAreEvicted() async { + let repository = FileTreeViewStateRepository(defaults: makeDefaults(), capacity: 2) + await repository.save(FileTreeViewState(expandedPaths: ["a"]), for: "one") + await repository.save(FileTreeViewState(expandedPaths: ["b"]), for: "two") + await repository.save(FileTreeViewState(expandedPaths: ["a", "c"]), for: "one") + await repository.save(FileTreeViewState(expandedPaths: ["d"]), for: "three") + #expect(await repository.state(for: "two") == nil) + #expect(await repository.state(for: "one")?.expandedPaths == ["a", "c"]) + #expect(await repository.state(for: "three") != nil) + } + + @Test func pathListsAreCapped() async { + let repository = FileTreeViewStateRepository(defaults: makeDefaults(), maxPathsPerState: 3) + await repository.save(FileTreeViewState(expandedPaths: (0..<10).map { "d\($0)" }), for: "s") + #expect(await repository.state(for: "s")?.expandedPaths.count == 3) + } + + @Test func relativePathsRoundTrip() { + #expect(FileTreeViewState.relativePath("/p/src/a", root: "/p") == "src/a") + #expect(FileTreeViewState.relativePath("/p", root: "/p") == "") + #expect(FileTreeViewState.relativePath("/other", root: "/p") == nil) + #expect(FileTreeViewState.relativePath("/pp/x", root: "/p") == nil) + #expect(FileTreeViewState.absolutePath("src/a", root: "/p") == "/p/src/a") + #expect(FileTreeViewState.absolutePath("etc", root: "/") == "/etc") + #expect(FileTreeViewState.relativePath("/etc", root: "/") == "etc") + } +} diff --git a/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/LocalFileTreeProviderTests.swift b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/LocalFileTreeProviderTests.swift new file mode 100644 index 000000000000..09ccd89bea81 --- /dev/null +++ b/Packages/macOS/CmuxFileTree/Tests/CmuxFileTreeTests/LocalFileTreeProviderTests.swift @@ -0,0 +1,94 @@ +import Darwin +import Foundation +import Testing +@testable import CmuxFileTree + +@Suite struct LocalFileTreeProviderTests { + private func makeTemporaryDirectory() throws -> URL { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-file-tree-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + return url + } + + @Test func listsKindsSizesAndHiddenFlags() async throws { + let root = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let fm = FileManager.default + try Data("hello".utf8).write(to: root.appendingPathComponent("a.txt")) + try Data().write(to: root.appendingPathComponent(".env")) + try fm.createDirectory(at: root.appendingPathComponent("src"), withIntermediateDirectories: false) + try fm.createSymbolicLink(atPath: root.appendingPathComponent("link-dir").path, withDestinationPath: "src") + try fm.createSymbolicLink(atPath: root.appendingPathComponent("link-file").path, withDestinationPath: "a.txt") + try fm.createSymbolicLink(atPath: root.appendingPathComponent("dangling").path, withDestinationPath: "missing") + let flagged = root.appendingPathComponent("flagged") + try Data().write(to: flagged) + #expect(chflags(flagged.path, UInt32(UF_HIDDEN)) == 0) + + let listing = try await LocalFileTreeProvider().listDirectory(at: root.path) + let byName = Dictionary(uniqueKeysWithValues: listing.entries.map { ($0.name, $0) }) + #expect(Set(byName.keys) == ["a.txt", ".env", "src", "link-dir", "link-file", "dangling", "flagged"]) + #expect(byName["a.txt"]?.kind == .file) + #expect(byName["a.txt"]?.size == 5) + #expect(byName["a.txt"]?.path == root.path + "/a.txt") + #expect(byName["a.txt"]?.modificationTime != nil) + #expect(byName["src"]?.kind == .directory) + #expect(byName["link-dir"]?.kind == .symbolicLinkToDirectory) + #expect(byName["link-dir"]?.isDirectory == true) + #expect(byName["link-file"]?.kind == .symbolicLink) + #expect(byName["dangling"]?.kind == .symbolicLink) + #expect(byName[".env"]?.isHidden == true) + #expect(byName["flagged"]?.isHidden == true) + #expect(byName["a.txt"]?.isHidden == false) + } + + @Test func listsUnicodeNamesByteForByte() async throws { + let root = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let name = "caf\u{00E9} \u{65E5}\u{672C}.md" + try Data().write(to: root.appendingPathComponent(name)) + let listing = try await LocalFileTreeProvider().listDirectory(at: root.path) + #expect(listing.entries.count == 1) + #expect(listing.entries.first?.name.precomposedStringWithCanonicalMapping == name) + } + + @Test func missingDirectoryThrows() async { + await #expect(throws: (any Error).self) { + _ = try await LocalFileTreeProvider().listDirectory(at: "/nonexistent-\(UUID().uuidString)") + } + } + + @Test func fsEventsReportTheChangedDirectory() async throws { + let root = try makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + try FileManager.default.createDirectory(at: root.appendingPathComponent("src"), withIntermediateDirectories: false) + let provider = LocalFileTreeProvider(eventLatency: 0.05) + let stream = try #require(provider.changes(under: root.path)) + let target = root.path + "/src" + let observed = Task { () -> Bool in + for await batch in stream where batch.directories.contains(target) || batch.subtrees.contains(target) { + return true + } + return false + } + // FSEvents needs the stream running before the write it should see; + // write repeatedly until the event arrives or the deadline passes. + let deadline = ContinuousClock.now.advanced(by: .seconds(10)) + let writer = Task { + var counter = 0 + while !Task.isCancelled && ContinuousClock.now < deadline { + counter += 1 + try? Data().write(to: root.appendingPathComponent("src/file\(counter)")) + try? await Task.sleep(for: .milliseconds(100)) + } + } + let timeout = Task { + try? await Task.sleep(until: deadline, clock: .continuous) + observed.cancel() + } + let sawEvent = await observed.value + writer.cancel() + timeout.cancel() + #expect(sawEvent) + } +} diff --git a/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift b/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift index e9122fc5b2f9..546fd7f78f1b 100644 --- a/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift +++ b/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift @@ -2007,10 +2007,19 @@ enum CmuxEmbeddedConfigSchema { }, "rightSidebar": { "title": "rightSidebar", - "description": "Legacy and extension-owned right-sidebar configuration preserved in cmux.json.", - "descriptionKey": "schemaDescriptions.rightSidebar", + "description": "Right-sidebar chrome settings. The object also keeps legacy and extension-owned right-sidebar configuration.", + "descriptionKey": "schemaDescriptions.rightSidebar.description", "type": "object", - "additionalProperties": true + "additionalProperties": true, + "properties": { + "toggleButton": { + "type": "string", + "enum": ["titlebar", "paneTabBar", "sidebarFooter", "hidden"], + "default": "titlebar", + "description": "Where the persistent right-sidebar show/hide button lives. `titlebar` puts it in the window's top-right corner, where it stays at one position whether the sidebar is shown or hidden. `paneTabBar` puts it at the end of the top-right pane's tab bar. `sidebarFooter` puts it in the left sidebar footer. `hidden` removes it; the mode bar button and the Toggle Right Sidebar shortcut still work.", + "descriptionKey": "schemaDescriptions.rightSidebar.toggleButton" + } + } }, "shortcuts": { "x-cmux-scopes": ["global"], @@ -2153,6 +2162,10 @@ enum CmuxEmbeddedConfigSchema { "toggleFileExplorer", "fileExplorerOpenSelection", "fileExplorerOpenSelectionFinderAlias", + "fileExplorerQuickLook", + "fileExplorerRenameSelection", + "fileExplorerToggleHiddenFiles", + "fileExplorerSelectParent", "saveFilePreview", "toggleFileEditorWordWrap", "openBrowser", @@ -2199,6 +2212,18 @@ enum CmuxEmbeddedConfigSchema { "fileExplorerOpenSelectionFinderAlias": { "$ref": "#/$defs/singleBareFirstStrokeShortcutBindingNullable" }, + "fileExplorerQuickLook": { + "$ref": "#/$defs/singleBareFirstStrokeShortcutBindingNullable" + }, + "fileExplorerRenameSelection": { + "$ref": "#/$defs/singleBareFirstStrokeShortcutBindingNullable" + }, + "fileExplorerToggleHiddenFiles": { + "$ref": "#/$defs/singleBareFirstStrokeShortcutBindingNullable" + }, + "fileExplorerSelectParent": { + "$ref": "#/$defs/singleBareFirstStrokeShortcutBindingNullable" + }, "diffViewerScrollDown": { "$ref": "#/$defs/bareFirstStrokeShortcutBindingNullable" }, diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/RightSidebarCatalogSection.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/RightSidebarCatalogSection.swift new file mode 100644 index 000000000000..425c74eaac53 --- /dev/null +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/RightSidebarCatalogSection.swift @@ -0,0 +1,17 @@ +import Foundation + +/// Settings under the dotted-id prefix `rightSidebar.*`. +/// +/// The `rightSidebar` object in `cmux.json` also holds legacy and +/// extension-owned configuration, so only the keys declared here are settings. +public struct RightSidebarCatalogSection: SettingCatalogSection { + /// Where the persistent right-sidebar show/hide button lives. + public let toggleButton = DefaultsKey( + id: "rightSidebar.toggleButton", + defaultValue: .titlebar, + userDefaultsKey: "rightSidebar.toggleButton" + ) + + /// Creates the right-sidebar settings section. + public init() {} +} diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift index 8b191e770f92..c4f7cc363617 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift @@ -26,6 +26,8 @@ public struct SettingCatalog: SettingCatalogSection { public let terminal = TerminalCatalogSection() public let notifications = NotificationsCatalogSection() public let sidebar = SidebarCatalogSection() + /// Settings for right-sidebar chrome (the `rightSidebar.*` keys). + public let rightSidebar = RightSidebarCatalogSection() public let sidebarAppearance = SidebarAppearanceCatalogSection() /// Settings for cmux pane divider and focused-pane chrome. public let paneChrome = PaneChromeCatalogSection() diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/RightSidebarToggleButtonPlacement.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/RightSidebarToggleButtonPlacement.swift new file mode 100644 index 000000000000..d564c15b37e3 --- /dev/null +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/RightSidebarToggleButtonPlacement.swift @@ -0,0 +1,22 @@ +import Foundation + +/// Where the persistent right-sidebar show/hide button lives +/// (`rightSidebar.toggleButton`). +/// +/// Every visible placement keeps the button on screen while the right sidebar +/// is hidden, so the button can always reopen it. +public enum RightSidebarToggleButtonPlacement: String, CaseIterable, Sendable, SettingCodable { + /// The window's top-trailing corner. The button keeps one screen position + /// whether the sidebar is shown or hidden, and replaces the mode bar's + /// close button while the sidebar is shown. + case titlebar + /// The trailing end of the top-right pane's tab bar, next to the tab bar + /// action buttons. The button follows that pane's edge. + case paneTabBar + /// The trailing end of the left sidebar footer. Hidden while the left + /// sidebar is hidden. + case sidebarFooter + /// No persistent button. The mode bar's close button and the shortcut + /// still work. + case hidden +} diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift index f74930069608..45b067e666b7 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift @@ -192,6 +192,10 @@ extension ShortcutAction { case .toggleRightSidebar: return ShortcutStroke(key: "b", command: true, option: true) case .fileExplorerOpenSelection: return ShortcutStroke(key: "\r") case .fileExplorerOpenSelectionFinderAlias: return ShortcutStroke(key: "↓", command: true) + case .fileExplorerQuickLook: return ShortcutStroke(key: "space") + case .fileExplorerRenameSelection: return ShortcutStroke(key: "f2") + case .fileExplorerToggleHiddenFiles: return ShortcutStroke(key: ".", command: true, shift: true) + case .fileExplorerSelectParent: return ShortcutStroke(key: "↑", command: true) case .openDiffViewer: return ShortcutStroke(key: "d", command: true, shift: true, control: true) case .saveFilePreview: return ShortcutStroke(key: "s", command: true) case .toggleFileEditorWordWrap: return ShortcutStroke(key: "z", option: true) diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+DisplayName.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+DisplayName.swift index 04a981a7cbcd..76537e7ec087 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+DisplayName.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+DisplayName.swift @@ -148,6 +148,14 @@ extension ShortcutAction { return String(localized: "shortcut.fileExplorerOpenSelection.label", defaultValue: "File Explorer: Open Selection") case .fileExplorerOpenSelectionFinderAlias: return String(localized: "shortcut.fileExplorerOpenSelectionFinderAlias.label", defaultValue: "File Explorer: Open Selection (Finder Alias)") + case .fileExplorerQuickLook: + return String(localized: "shortcut.fileExplorerQuickLook.label", defaultValue: "File Explorer: Quick Look") + case .fileExplorerRenameSelection: + return String(localized: "shortcut.fileExplorerRenameSelection.label", defaultValue: "File Explorer: Rename") + case .fileExplorerToggleHiddenFiles: + return String(localized: "shortcut.fileExplorerToggleHiddenFiles.label", defaultValue: "File Explorer: Show Hidden Files") + case .fileExplorerSelectParent: + return String(localized: "shortcut.fileExplorerSelectParent.label", defaultValue: "File Explorer: Enclosing Folder") case .toggleCanvasLayout: return String(localized: "shortcut.toggleCanvasLayout.label", defaultValue: "Toggle Canvas Layout") case .canvasRevealFocusedPane: diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Group.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Group.swift index a8b85e4eb1bc..b7e658d202ff 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Group.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Group.swift @@ -38,6 +38,7 @@ extension ShortcutAction { .equalizeSplits, .resizePaneLeft, .resizePaneRight, .resizePaneUp, .resizePaneDown, .splitBrowserRight, .splitBrowserDown, .toggleRightSidebar, .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias, + .fileExplorerQuickLook, .fileExplorerRenameSelection, .fileExplorerToggleHiddenFiles, .fileExplorerSelectParent, .toggleCanvasLayout, .canvasRevealFocusedPane, .canvasOverview, .canvasZoomIn, .canvasZoomOut, .canvasZoomReset, .canvasTidy, .canvasAlignLeft, .canvasAlignRight, .canvasAlignTop, .canvasAlignBottom, diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+SettingsVisibleActions.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+SettingsVisibleActions.swift index 8d7c76bb0f60..58f8a3671e84 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+SettingsVisibleActions.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+SettingsVisibleActions.swift @@ -16,6 +16,10 @@ private extension ShortcutAction { .findInDirectory, .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias, + .fileExplorerQuickLook, + .fileExplorerRenameSelection, + .fileExplorerToggleHiddenFiles, + .fileExplorerSelectParent, ].filter(actions.contains) let actionSet = Set(colocatedSidebarActions) let baseActions = actions.filter { !actionSet.contains($0) } diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift index 74fb99cf9ad2..11125855361c 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift @@ -150,6 +150,14 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod case fileExplorerOpenSelection /// Mirrors Finder's Command-Down open-selection shortcut from File Explorer focus. case fileExplorerOpenSelectionFinderAlias + /// Previews the selected File Explorer items with Quick Look. + case fileExplorerQuickLook + /// Renames the selected File Explorer item inline. + case fileExplorerRenameSelection + /// Shows or hides hidden files in the File Explorer. + case fileExplorerToggleHiddenFiles + /// Selects the enclosing folder of the File Explorer selection. + case fileExplorerSelectParent // MARK: Canvas case toggleCanvasLayout @@ -273,7 +281,11 @@ extension ShortcutAction { .diffViewerNextFile, .diffViewerPreviousFile, .fileExplorerOpenSelection, - .fileExplorerOpenSelectionFinderAlias: + .fileExplorerOpenSelectionFinderAlias, + .fileExplorerQuickLook, + .fileExplorerRenameSelection, + .fileExplorerToggleHiddenFiles, + .fileExplorerSelectParent: return true default: return false @@ -285,6 +297,10 @@ extension ShortcutAction { self != .showHideAllWindows && self != .fileExplorerOpenSelection && self != .fileExplorerOpenSelectionFinderAlias + && self != .fileExplorerQuickLook + && self != .fileExplorerRenameSelection + && self != .fileExplorerToggleHiddenFiles + && self != .fileExplorerSelectParent && self != .cycleTextBoxSubmitAction } @@ -301,7 +317,7 @@ extension ShortcutAction { .switchRightSidebarToSessions, .switchRightSidebarToFeed, .switchRightSidebarToDock, .switchRightSidebarToMachines: return .atom(.sidebarFocus) - case .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias: + case .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias, .fileExplorerQuickLook, .fileExplorerRenameSelection, .fileExplorerToggleHiddenFiles, .fileExplorerSelectParent: return .atom(.sidebarFocus) case .commandPaletteNext, .commandPalettePrevious: return .key(ShortcutContextKnownKey.commandPaletteVisible.rawValue) diff --git a/Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/ShortcutActionNumberedDigitTests.swift b/Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/ShortcutActionNumberedDigitTests.swift index 561bf958fc77..c9994c07d90c 100644 --- a/Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/ShortcutActionNumberedDigitTests.swift +++ b/Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/ShortcutActionNumberedDigitTests.swift @@ -48,6 +48,18 @@ struct ShortcutActionNumberedDigitTests { first: ShortcutStroke(key: "↓", command: true) ) ) + #expect(ShortcutAction.fileExplorerQuickLook.defaultShortcut == StoredShortcut(first: ShortcutStroke(key: "space"))) + #expect(ShortcutAction.fileExplorerRenameSelection.defaultShortcut == StoredShortcut(first: ShortcutStroke(key: "f2"))) + #expect( + ShortcutAction.fileExplorerToggleHiddenFiles.defaultShortcut == StoredShortcut( + first: ShortcutStroke(key: ".", command: true, shift: true) + ) + ) + #expect( + ShortcutAction.fileExplorerSelectParent.defaultShortcut == StoredShortcut( + first: ShortcutStroke(key: "↑", command: true) + ) + ) } @Test func onlyFocusedContentActionsAllowBareFirstStrokes() { @@ -65,6 +77,10 @@ struct ShortcutActionNumberedDigitTests { .diffViewerPreviousFile, .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias, + .fileExplorerQuickLook, + .fileExplorerRenameSelection, + .fileExplorerToggleHiddenFiles, + .fileExplorerSelectParent, ] for action in ShortcutAction.allCases { diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/ShortcutListRestoreValidationTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/ShortcutListRestoreValidationTests.swift index 3fefae476365..4b069c8ddc3d 100644 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/ShortcutListRestoreValidationTests.swift +++ b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/ShortcutListRestoreValidationTests.swift @@ -21,8 +21,13 @@ import Testing let store = JSONConfigStore(fileURL: directory.appendingPathComponent("cmux.json")) let catalog = SettingCatalog() let shortcut = StoredShortcut(first: ShortcutStroke(key: "space")) + // Files Quick Look defaults to bare Space in the sidebar; a user who + // kept a legacy app-wide Space binding has unbound it. try await store.set( - [ShortcutAction.openSettings.rawValue: shortcut], + [ + ShortcutAction.openSettings.rawValue: shortcut, + ShortcutAction.fileExplorerQuickLook.rawValue: .unbound, + ], for: catalog.shortcuts.bindings ) let model = ShortcutListModel( diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 0f9f95b0b71c..f6a41b49b236 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -234922,256 +234922,6 @@ } } }, - "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": { @@ -237567,419 +237317,6 @@ } } }, - "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": { @@ -562403,131 +561740,6 @@ } } }, - "cli.sshPtyAttach.launchAcknowledgementTimeoutRetry": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux did not acknowledge the SSH launch; retrying %s in %ss." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux hat den SSH-Start nicht bestätigt; neuer Versuch %s in %ss." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux n’a pas confirmé le démarrage SSH ; nouvelle tentative %s dans %ss." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "[cmux] لم يؤكد cmux بدء SSH؛ ستتم إعادة المحاولة %s خلال %s ث." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux no confirmó el inicio de SSH; reintentando %s en %ss." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux 未確認 SSH 啟動;將在第 %s 次於 %s 秒後重試。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux 未确认 SSH 启动;将在第 %s 次于 %s 秒后重试。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux가 SSH 시작을 확인하지 못했습니다. %s번째로 %s초 후 다시 시도합니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux が SSH の起動を確認できないため、%s 回目を %s 秒後に再試行します。" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux nije potvrdio SSH pokretanje; ponovni pokušaj %s za %ss." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux bekræftede ikke SSH-starten; prøver igen %s om %ss." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux non ha confermato l’avvio SSH; nuovo tentativo %s tra %ss." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux មិនបានបញ្ជាក់ការចាប់ផ្តើម SSH ទេ; កំពុងព្យាយាមម្តងទៀត %s ក្នុងរយៈពេល %ss។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux bekreftet ikke SSH-oppstarten; prøver på nytt %s om %ss." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux nie potwierdził uruchomienia SSH; ponawianie próby %s za %ss." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "[cmux] o cmux não confirmou o início do SSH; tentando novamente %s em %ss." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux не подтвердил запуск SSH; повторная попытка %s через %ss." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux ไม่ยืนยันการเริ่มต้น SSH; กำลังลองใหม่ครั้งที่ %s ในอีก %s วินาที" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux SSH başlatmasını onaylamadı; %s saniye içinde %s. deneme yapılıyor." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "[cmux] cmux не підтвердив запуск SSH; повторна спроба %s через %s с." - } - } - } - }, "devices.discovery.settingsDisabled": { "extractionState": "manual", "localizations": { @@ -564298,14634 +563510,20205 @@ } } }, - "settings.automation.pi": { + "cloudTree.group.cloudMachines": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Pi Integration" + "value": "Cloud Machines" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "تكامل Pi" + "value": "Cloud-Maschinen" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Pi-Integration" + "value": "Machines cloud" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Integración de Pi" + "value": "أجهزة سحابية" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Intégration de Pi" + "value": "Máquinas en la nube" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Pi連携" + "value": "雲端機器" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Pi 연동" + "value": "云端机器" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "Pi 集成" + "value": "클라우드 머신" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "Pi 整合" + "value": "クラウドマシン" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Pi integracija" + "value": "Mašine u oblaku" } }, "da": { "stringUnit": { "state": "translated", - "value": "Pi-integration" + "value": "Cloud-maskiner" } }, "it": { "stringUnit": { "state": "translated", - "value": "Integrazione di Pi" + "value": "Macchine cloud" } }, "km": { "stringUnit": { "state": "translated", - "value": "ការរួមបញ្ចូល Pi" + "value": "ម៉ាស៊ីនលើពពក" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Pi-integrasjon" + "value": "Skymaskiner" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Integracja Pi" + "value": "Maszyny w chmurze" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Integração do Pi" + "value": "Máquinas na nuvem" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Интеграция Pi" + "value": "Облачные машины" } }, "th": { "stringUnit": { "state": "translated", - "value": "การผสานรวม Pi" + "value": "เครื่องคลาวด์" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Pi entegrasyonu" + "value": "Bulut makineleri" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Інтеграція Pi" + "value": "Хмарні машини" } } } }, - "settings.automation.pi.note": { + "cloudTree.group.devices": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "When enabled, cmux wraps the pi command and loads its bundled session extension without changing your Pi configuration. Disable if you prefer to manage Pi hooks yourself." + "value": "My Devices" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "عند التفعيل، يغلّف cmux أمر pi ويحمّل امتداد الجلسة المضمّن دون تغيير إعدادات Pi. عطّل هذا الخيار إذا كنت تفضّل إدارة خطافات Pi بنفسك." + "value": "Meine Geräte" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Wenn aktiviert, umhüllt cmux den pi-Befehl und lädt seine gebündelte Sitzungserweiterung, ohne deine Pi-Konfiguration zu ändern. Deaktiviere dies, wenn du die Pi-Hooks selbst verwalten möchtest." + "value": "Mes appareils" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Cuando está activado, cmux envuelve el comando pi y carga su extensión de sesión integrada sin cambiar tu configuración de Pi. Desactívalo si prefieres gestionar tú mismo los hooks de Pi." + "value": "أجهزتي" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Lorsque cette option est activée, cmux encapsule la commande pi et charge son extension de session intégrée sans modifier votre configuration Pi. Désactivez-la si vous préférez gérer les hooks Pi vous-même." + "value": "Mis dispositivos" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "有効にすると、cmuxはpiコマンドをラップし、Piの設定を変更せずにバンドルされたセッション拡張機能を読み込みます。Piのフックを自分で管理する場合は無効にしてください。" + "value": "我的裝置" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "활성화하면 cmux가 pi 명령을 래핑하고 Pi 설정을 변경하지 않고 번들 세션 확장을 로드합니다. Pi 훅을 직접 관리하려면 비활성화하세요." + "value": "我的设备" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "启用后,cmux 会包装 pi 命令并加载内置会话扩展,不会更改你的 Pi 配置。如果你希望自行管理 Pi 钩子,请禁用此选项。" + "value": "내 기기" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "啟用後,cmux 會包裝 pi 指令並載入內建工作階段擴充功能,不會變更你的 Pi 設定。若偏好自行管理 Pi 掛鉤,請停用此選項。" + "value": "マイデバイス" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Kada je omogućeno, cmux obavija naredbu pi i učitava ugrađeno proširenje sesije bez mijenjanja vaše Pi konfiguracije. Onemogućite ako želite sami upravljati Pi kukama." + "value": "Moji uređaji" } }, "da": { "stringUnit": { "state": "translated", - "value": "Når funktionen er aktiveret, indpakker cmux pi-kommandoen og indlæser den medfølgende sessionsudvidelse uden at ændre din Pi-konfiguration. Deaktiver den, hvis du selv vil administrere Pi-hooks." + "value": "Mine enheder" } }, "it": { "stringUnit": { "state": "translated", - "value": "Quando è abilitata, cmux avvolge il comando pi e carica l'estensione di sessione inclusa senza modificare la configurazione di Pi. Disabilitala se preferisci gestire autonomamente gli hook di Pi." + "value": "I miei dispositivi" } }, "km": { "stringUnit": { "state": "translated", - "value": "នៅពេលបើកដំណើរការ cmux នឹងរុំពាក្យបញ្ជា pi ហើយផ្ទុកផ្នែកបន្ថែមសម័យដែលភ្ជាប់មកជាមួយ ដោយមិនផ្លាស់ប្តូរការកំណត់ Pi របស់អ្នក។ បិទវា ប្រសិនបើអ្នកចង់គ្រប់គ្រង hooks របស់ Pi ដោយខ្លួនឯង។" + "value": "ឧបករណ៍របស់ខ្ញុំ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Når dette er aktivert, pakker cmux inn pi-kommandoen og laster den medfølgende øktutvidelsen uten å endre Pi-konfigurasjonen din. Deaktiver hvis du vil administrere Pi-hookene selv." + "value": "Mine enheter" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Po włączeniu cmux opakuje polecenie pi i załaduje dołączone rozszerzenie sesji bez zmieniania konfiguracji Pi. Wyłącz tę opcję, jeśli wolisz samodzielnie zarządzać hookami Pi." + "value": "Moje urządzenia" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Quando ativado, o cmux envolve o comando pi e carrega a extensão de sessão incluída sem alterar sua configuração do Pi. Desative se preferir gerenciar os hooks do Pi por conta própria." + "value": "Meus dispositivos" } }, "ru": { "stringUnit": { "state": "translated", - "value": "При включении cmux оборачивает команду pi и загружает встроенное расширение сеанса, не изменяя конфигурацию Pi. Отключите этот параметр, если предпочитаете управлять хуками Pi самостоятельно." + "value": "Мои устройства" } }, "th": { "stringUnit": { "state": "translated", - "value": "เมื่อเปิดใช้งาน cmux จะครอบคำสั่ง pi และโหลดส่วนขยายเซสชันที่มาพร้อมกับโปรแกรมโดยไม่เปลี่ยนการตั้งค่า Pi ของคุณ ปิดใช้งานหากต้องการจัดการ hooks ของ Pi เอง" + "value": "อุปกรณ์ของฉัน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Etkinleştirildiğinde cmux, Pi yapılandırmanızı değiştirmeden pi komutunu sarar ve birlikte gelen oturum uzantısını yükler. Pi kancalarını kendiniz yönetmeyi tercih ediyorsanız devre dışı bırakın." + "value": "Cihazlarım" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Якщо ввімкнено, cmux обгортає команду pi й завантажує вбудоване розширення сеансу, не змінюючи конфігурацію Pi. Вимкніть цей параметр, якщо хочете самостійно керувати хуками Pi." + "value": "Мої пристрої" } } } }, - "settings.automation.pi.subtitleOff": { + "machines.listUnavailable.stale": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Pi runs without cmux integration." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يعمل Pi بدون تكامل cmux." + "value": "Machine list unavailable — showing last known" } }, "de": { "stringUnit": { "state": "translated", - "value": "Pi läuft ohne cmux-Integration." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Pi se ejecuta sin integración con cmux." + "value": "Maschinenliste nicht verfügbar – zeige letzten bekannten Stand" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Pi s’exécute sans intégration cmux." + "value": "Liste des machines indisponible — affichage des dernières informations connues" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "Piはcmux連携なしで実行されます。" + "value": "قائمة الأجهزة غير متاحة — يتم عرض آخر حالة معروفة" } }, - "ko": { + "es": { "stringUnit": { "state": "translated", - "value": "Pi가 cmux 연동 없이 실행됩니다." + "value": "Lista de máquinas no disponible — se muestra el último estado conocido" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Pi 在没有 cmux 集成的情况下运行。" + "value": "無法取得機器清單 — 顯示上次已知的狀態" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Pi 會在不使用 cmux 整合的情況下執行。" + "value": "机器列表不可用 —— 显示的是最后已知状态" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Pi radi bez cmux integracije." + "value": "머신 목록을 사용할 수 없음 — 마지막으로 확인된 상태 표시 중" } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Pi kører uden cmux-integration." + "value": "マシン一覧を取得できません — 最後の状態を表示中" } - }, - "it": { + } + } + }, + "machines.listUnavailable.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Pi viene eseguito senza l'integrazione con cmux." + "value": "Your machines are unchanged. cmux couldn’t load the list from the Cloud service and retries on its own." } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "Pi ដំណើរការដោយគ្មានការរួមបញ្ចូល cmux។" + "value": "Deine Maschinen sind unverändert. cmux konnte die Liste nicht vom Cloud-Dienst laden und versucht es selbstständig erneut." } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Pi kjører uten cmux-integrasjon." + "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." } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Pi działa bez integracji z cmux." + "value": "لم تتغير أجهزتك. تعذر على cmux تحميل القائمة من الخدمة السحابية، وسيعيد المحاولة تلقائيًا." } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "O Pi é executado sem a integração com o cmux." + "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." } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Pi работает без интеграции с cmux." + "value": "您的機器沒有任何變動。cmux 無法從雲端服務載入清單,會自動重試。" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Pi ทำงานโดยไม่มีการผสานรวมกับ cmux" + "value": "你的机器没有任何变化。cmux 无法从云端服务加载列表,会自动重试。" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Pi, cmux entegrasyonu olmadan çalışır." + "value": "머신은 변경되지 않았습니다. cmux가 클라우드 서비스에서 목록을 불러오지 못했으며 자동으로 다시 시도합니다." } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Pi працює без інтеграції з cmux." + "value": "マシンは変更されていません。cmux はクラウドサービスから一覧を読み込めませんでした。自動的に再試行します。" } } } }, - "settings.automation.pi.subtitleOn": { + "machines.listUnavailable.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sidebar shows Pi session status and notifications." + "value": "Can’t load the machine list" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "يعرض الشريط الجانبي حالة جلسة Pi والإشعارات." + "value": "Maschinenliste kann nicht geladen werden" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Die Seitenleiste zeigt den Pi-Sitzungsstatus und Benachrichtigungen an." + "value": "Impossible de charger la liste des machines" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذر تحميل قائمة الأجهزة" } }, "es": { "stringUnit": { "state": "translated", - "value": "La barra lateral muestra el estado de la sesión de Pi y las notificaciones." + "value": "No se puede cargar la lista de máquinas" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "La barre latérale affiche le statut de la session Pi et les notifications." + "value": "無法載入機器清單" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "サイドバーにPiセッションのステータスと通知が表示されます。" + "value": "无法加载机器列表" } }, "ko": { "stringUnit": { "state": "translated", - "value": "사이드바에 Pi 세션 상태와 알림이 표시됩니다." + "value": "머신 목록을 불러올 수 없습니다" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "侧边栏显示 Pi 会话状态和通知。" + "value": "マシン一覧を読み込めません" + } + } + } + }, + "machines.offline.stale": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Offline — showing last known" } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "側邊欄顯示 Pi 工作階段狀態和通知。" + "value": "Offline – zeige letzten bekannten Stand" } }, - "bs": { + "fr": { "stringUnit": { "state": "translated", - "value": "Bočna traka prikazuje status Pi sesije i obavijesti." + "value": "Hors ligne — affichage des dernières informations connues" } }, - "da": { + "ar": { "stringUnit": { "state": "translated", - "value": "Sidepanelet viser Pi-sessionens status og notifikationer." + "value": "غير متصل — يتم عرض آخر حالة معروفة" } }, - "it": { + "es": { "stringUnit": { "state": "translated", - "value": "La barra laterale mostra lo stato della sessione Pi e le notifiche." + "value": "Sin conexión — se muestra el último estado conocido" } }, - "km": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "របារចំហៀងបង្ហាញស្ថានភាពសម័យ Pi និងការជូនដំណឹង។" + "value": "離線 — 顯示上次已知的狀態" } }, - "nb": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Sidepanelet viser status og varsler for Pi-økten." + "value": "离线 —— 显示的是最后已知状态" } }, - "pl": { + "ko": { "stringUnit": { "state": "translated", - "value": "Pasek boczny pokazuje stan sesji Pi i powiadomienia." + "value": "오프라인 — 마지막으로 확인된 상태 표시 중" } }, - "pt-BR": { + "ja": { "stringUnit": { "state": "translated", - "value": "A barra lateral mostra o status da sessão do Pi e as notificações." + "value": "オフライン — 最後の状態を表示中" + } + } + } + }, + "machines.offline.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud machines load when this Mac is back online." } }, - "ru": { + "de": { "stringUnit": { "state": "translated", - "value": "На боковой панели отображаются состояние сеанса Pi и уведомления." + "value": "Cloud-Maschinen werden geladen, sobald dieser Mac wieder online ist." } }, - "th": { + "fr": { "stringUnit": { "state": "translated", - "value": "แถบด้านข้างจะแสดงสถานะเซสชัน Pi และการแจ้งเตือน" + "value": "Les machines Cloud se chargeront dès que ce Mac sera de nouveau en ligne." } }, - "tr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Kenar çubuğu, Pi oturumunun durumunu ve bildirimleri gösterir." + "value": "سيتم تحميل الأجهزة السحابية عندما يعود جهاز Mac هذا إلى الاتصال بالإنترنت." } }, - "uk": { + "es": { "stringUnit": { "state": "translated", - "value": "Бічна панель показує стан сеансу Pi та сповіщення." + "value": "Las máquinas en la nube se cargarán cuando este Mac vuelva a estar en línea." } - } - } - }, - "settings.search.alias.setting.automation.pi": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration pi hooks agent integration status notifications" + "value": "這台 Mac 恢復連線後,就會載入雲端機器。" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration تكامل Pi خطافات وكيل حالة إشعارات" + "value": "这台 Mac 恢复联网后,就会加载云端机器。" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi kukice integracija agenta status obavijesti" + "value": "이 Mac이 다시 온라인 상태가 되면 클라우드 머신을 불러옵니다." } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi-hooks agentintegration status notifikationer" + "value": "この Mac がオンラインに戻ると Cloud マシンを読み込みます。" + } + } + } + }, + "machines.offline.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for network" } }, "de": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi Hooks Agent Integration Status Benachrichtigungen" + "value": "Warte auf Netzwerk" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi hooks integración de agente estado notificaciones" + "value": "En attente du réseau" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi hooks intégration agent état notifications" + "value": "في انتظار الشبكة" } }, - "it": { + "es": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration hook Pi integrazione agente stato notifiche" + "value": "Esperando la red" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi フック エージェント連携 ステータス 通知" + "value": "正在等待網路" } }, - "km": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration hooks Pi ការរួមបញ្ចូលភ្នាក់ងារ ស្ថានភាព ការជូនដំណឹង" + "value": "正在等待网络" } }, "ko": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi 훅 에이전트 연동 상태 알림" + "value": "네트워크 대기 중" } }, - "nb": { + "ja": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi-hook agentintegrasjon status varsler" + "value": "ネットワークを待機中" } - }, - "pl": { + } + } + }, + "machines.reconnecting.title": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration hooki Pi integracja agenta stan powiadomienia" + "value": "Reconnecting to Cloud…" } }, - "pt-BR": { + "de": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration hooks do Pi integração do agente status notificações" + "value": "Verbindung zur Cloud wird wiederhergestellt…" } }, - "ru": { + "fr": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration хуки Pi интеграция агента состояние уведомления" + "value": "Reconnexion au Cloud…" } }, - "th": { + "ar": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration hooks ของ Pi การผสานรวมเอเจนต์ สถานะ การแจ้งเตือน" + "value": "جارٍ إعادة الاتصال بالسحابة…" } }, - "tr": { + "es": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi kancaları aracı entegrasyonu durum bildirimler" + "value": "Reconectando con la nube…" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration хуки Pi інтеграція агента стан сповіщення" + "value": "正在重新連線至雲端…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi 钩子 代理集成 状态 通知" + "value": "正在重新连接云端…" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "automation.piIntegration Pi 掛鉤 代理程式整合 狀態 通知" + "value": "클라우드에 다시 연결하는 중…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドに再接続中…" } } } }, - "cli.hooks.setup.wrapperNote": { + "cloud.ports.vpnOff.explanation": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": " (Claude Code and Pi hooks are injected automatically via their cmux wrappers)" + "value": "Ports open in cmux without setup. To use private addresses in other apps, set up Cloud VPN." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": " (يتم حقن خطافات Claude Code وPi تلقائيًا عبر أغلفة cmux)" + "value": "Ports lassen sich in cmux ohne Einrichtung öffnen. Richte Cloud-VPN ein, um private Adressen in anderen Apps zu verwenden." } }, - "bs": { + "fr": { "stringUnit": { "state": "translated", - "value": " (Claude Code i Pi kukice se automatski ubacuju putem cmux omotača)" + "value": "Les ports s’ouvrent dans cmux sans configuration. Pour utiliser les adresses privées dans d’autres apps, configurez le VPN Cloud." } }, - "da": { + "ar": { "stringUnit": { "state": "translated", - "value": " (Claude Code- og Pi-hooks indsættes automatisk via deres cmux-wrappere)" + "value": "تُفتح المنافذ في cmux دون إعداد. لاستخدام العناوين الخاصة في التطبيقات الأخرى، أعدّ Cloud VPN." } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": " (Claude-Code- und Pi-Hooks werden automatisch über ihre cmux-Wrapper eingefügt)" + "value": "Los puertos se abren en cmux sin configuración. Para usar direcciones privadas en otras apps, configura Cloud VPN." } }, - "es": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": " (Los hooks de Claude Code y Pi se inyectan automáticamente mediante sus wrappers de cmux)" + "value": "無需設定即可在 cmux 中開啟連接埠。如要在其他 App 使用私有位址,請設定 Cloud VPN。" } }, - "fr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": " (Les hooks de Claude Code et Pi sont injectés automatiquement par leurs wrappers cmux)" + "value": "无需设置即可在 cmux 中打开端口。要在其他应用中使用私有地址,请设置云 VPN。" } }, - "it": { + "ko": { "stringUnit": { "state": "translated", - "value": " (Gli hook di Claude Code e Pi vengono iniettati automaticamente tramite i rispettivi wrapper cmux)" + "value": "별도 설정 없이 cmux에서 포트를 열 수 있습니다. 다른 앱에서 비공개 주소를 사용하려면 Cloud VPN을 설정하세요." } }, "ja": { "stringUnit": { "state": "translated", - "value": " (Claude Code と Pi のフックは cmux ラッパーによって自動的に注入されます)" + "value": "設定なしで cmux 内のポートを開けます。他のアプリでプライベートアドレスを使うには、Cloud VPN を設定してください。" } }, - "km": { + "bs": { "stringUnit": { - "state": "translated", - "value": " (hooks របស់ Claude Code និង Pi ត្រូវបានបញ្ចូលដោយស្វ័យប្រវត្តិតាមរយៈ wrapper របស់ cmux)" + "state": "needs_review", + "value": "Portovi se otvaraju u cmux-u bez podešavanja. Za korištenje privatnih adresa u drugim aplikacijama podesite Cloud VPN." } }, - "ko": { + "da": { "stringUnit": { - "state": "translated", - "value": " (Claude Code 및 Pi 훅은 cmux 래퍼를 통해 자동으로 주입됩니다)" + "state": "needs_review", + "value": "Porte åbnes i cmux uden opsætning. Opsæt Cloud VPN for at bruge private adresser i andre apps." + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "Le porte si aprono in cmux senza configurazione. Per usare gli indirizzi privati in altre app, configura Cloud VPN." + } + }, + "km": { + "stringUnit": { + "state": "needs_review", + "value": "ច្រកអាចបើកក្នុង cmux ដោយមិនចាំបាច់រៀបចំ។ ដើម្បីប្រើអាសយដ្ឋានឯកជនក្នុងកម្មវិធីផ្សេងទៀត សូមរៀបចំ Cloud VPN។" } }, "nb": { "stringUnit": { - "state": "translated", - "value": " (Claude Code- og Pi-hooker settes inn automatisk via cmux-innpakningene)" + "state": "needs_review", + "value": "Porter åpnes i cmux uten oppsett. Sett opp Cloud VPN for å bruke private adresser i andre apper." } }, "pl": { "stringUnit": { - "state": "translated", - "value": " (Hooki Claude Code i Pi są automatycznie wstrzykiwane przez ich opakowania cmux)" + "state": "needs_review", + "value": "Porty otwierają się w cmux bez konfiguracji. Aby używać prywatnych adresów w innych aplikacjach, skonfiguruj Cloud VPN." } }, "pt-BR": { "stringUnit": { - "state": "translated", - "value": " (Os hooks do Claude Code e do Pi são injetados automaticamente pelos respectivos wrappers do cmux)" + "state": "needs_review", + "value": "As portas abrem no cmux sem configuração. Para usar endereços privados em outros apps, configure o Cloud VPN." } }, "ru": { "stringUnit": { - "state": "translated", - "value": " (Хуки Claude Code и Pi автоматически внедряются их оболочками cmux)" + "state": "needs_review", + "value": "Порты открываются в cmux без настройки. Чтобы использовать приватные адреса в других приложениях, настройте Cloud VPN." } }, "th": { "stringUnit": { - "state": "translated", - "value": " (hooks ของ Claude Code และ Pi จะถูกฉีดโดยอัตโนมัติผ่าน wrapper ของ cmux)" + "state": "needs_review", + "value": "เปิดพอร์ตใน cmux ได้โดยไม่ต้องตั้งค่า หากต้องการใช้ที่อยู่ส่วนตัวในแอปอื่น ให้ตั้งค่า Cloud VPN" } }, "tr": { "stringUnit": { - "state": "translated", - "value": " (Claude Code ve Pi kancaları, cmux sarmalayıcıları aracılığıyla otomatik olarak eklenir)" + "state": "needs_review", + "value": "Bağlantı noktaları cmux içinde kurulum gerektirmeden açılır. Diğer uygulamalarda özel adresleri kullanmak için Cloud VPN’i kurun." } }, "uk": { "stringUnit": { - "state": "translated", - "value": " (Хуки Claude Code і Pi автоматично додаються їхніми обгортками cmux)" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": " (Claude Code 和 Pi 的钩子会通过各自的 cmux 包装器自动注入)" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": " (Claude Code 和 Pi 的掛鉤會透過各自的 cmux 包裝器自動注入)" + "state": "needs_review", + "value": "Порти відкриваються в cmux без налаштування. Щоб використовувати приватні адреси в інших застосунках, налаштуйте Cloud VPN." } } } }, - "devices.link.error.notDiscoverable": { + "cloud.ports.vpnOff.title": { "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": "Optional: Cloud VPN" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "جهاز Mac هذا غير قابل للاكتشاف. على ذلك الجهاز، فعّل «جعل جهاز Mac هذا قابلاً للاكتشاف» في الإعدادات › الأجهزة." + "value": "Optional: Cloud-VPN" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Dieser Mac ist nicht auffindbar. Aktiviere auf diesem Mac unter „Einstellungen › Geräte“ die Option „Diesen Mac auffindbar machen“." + "value": "Facultatif : VPN Cloud" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Este Mac no es detectable. En ese Mac, activa «Hacer que este Mac sea detectable» en Ajustes › Dispositivos." + "value": "اختياري: Cloud VPN" } }, - "fr": { + "es": { "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": "Opcional: Cloud VPN" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "このMacは検出可能になっていません。そのMacの「設定 › デバイス」で「このMacを検出可能にする」をオンにしてください。" + "value": "選用:Cloud VPN" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "이 Mac을 검색할 수 없습니다. 해당 Mac의 설정 › 기기에서 “이 Mac을 검색할 수 있도록 설정”을 켜세요." + "value": "可选:云 VPN" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "这台 Mac 无法被发现。请在该 Mac 的“设置 › 设备”中开启“允许发现此 Mac”。" + "value": "선택 사항: Cloud VPN" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "這部 Mac 無法被探索。請在該 Mac 的「設定 › 裝置」中開啟「允許探索此 Mac」。" + "value": "任意:Cloud VPN" } }, "bs": { "stringUnit": { - "state": "translated", - "value": "Ovaj Mac nije vidljiv. Na tom Macu uključite „Učini ovaj Mac vidljivim” u Postavke › Uređaji." + "state": "needs_review", + "value": "Opcionalno: Cloud VPN" } }, "da": { "stringUnit": { - "state": "translated", - "value": "Denne Mac kan ikke findes. Slå “Gør denne Mac synlig” til på den Mac under Indstillinger › Enheder." + "state": "needs_review", + "value": "Valgfrit: Cloud VPN" } }, "it": { "stringUnit": { - "state": "translated", - "value": "Questo Mac non è individuabile. Su quel Mac, attiva “Rendi individuabile questo Mac” in Impostazioni › Dispositivi." + "state": "needs_review", + "value": "Facoltativo: Cloud VPN" } }, "km": { "stringUnit": { - "state": "translated", - "value": "Mac នេះមិនអាចរកឃើញបានទេ។ នៅលើ Mac នោះ សូមបើក “ធ្វើឲ្យ Mac នេះអាចរកឃើញ” ក្នុងការកំណត់ › ឧបករណ៍។" + "state": "needs_review", + "value": "ជាជម្រើស៖ Cloud VPN" } }, "nb": { "stringUnit": { - "state": "translated", - "value": "Denne Macen er ikke synlig. Slå på «Gjør denne Macen synlig» på den Macen under Innstillinger › Enheter." + "state": "needs_review", + "value": "Valgfritt: Cloud VPN" } }, "pl": { "stringUnit": { - "state": "translated", - "value": "Ten Mac nie jest wykrywalny. Na tym Macu włącz „Uczyń ten Mac wykrywalnym” w Ustawienia › Urządzenia." + "state": "needs_review", + "value": "Opcjonalnie: Cloud VPN" } }, "pt-BR": { "stringUnit": { - "state": "translated", - "value": "Este Mac não está detectável. Nesse Mac, ative “Tornar este Mac detectável” em Ajustes › Dispositivos." + "state": "needs_review", + "value": "Opcional: Cloud VPN" } }, "ru": { "stringUnit": { - "state": "translated", - "value": "Этот Mac недоступен для обнаружения. На том Mac включите «Сделать этот Mac доступным для обнаружения» в разделе Настройки › Устройства." + "state": "needs_review", + "value": "Необязательно: Cloud VPN" } }, "th": { "stringUnit": { - "state": "translated", - "value": "Mac เครื่องนี้ไม่สามารถค้นพบได้ บน Mac เครื่องนั้น ให้เปิด “ทำให้ Mac เครื่องนี้ค้นพบได้” ในการตั้งค่า › อุปกรณ์" + "state": "needs_review", + "value": "ตัวเลือกเพิ่มเติม: Cloud VPN" } }, "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." + "state": "needs_review", + "value": "İsteğe bağlı: Cloud VPN" } }, "uk": { "stringUnit": { - "state": "translated", - "value": "Цей Mac недоступний для виявлення. На тому Mac увімкніть «Зробити цей Mac доступним для виявлення» у розділі Налаштування › Пристрої." + "state": "needs_review", + "value": "Необов’язково: Cloud VPN" } } } }, - "cli.auth.status.teamNotLoaded": { + "cloud.vpn.setup.address.body": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": " team: not loaded" + "value": "Each machine has its own private IP. Two machines may both expose port 3000, for example 10.40.0.10:3000 and 10.40.0.11:3000. Right-click a Cloud port and choose Copy Private Address URL after connecting. These addresses are private, not public share links." } }, - "ja": { + "de": { "stringUnit": { "state": "translated", - "value": " チーム: 未読み込み" + "value": "Jede Maschine hat eine eigene private IP. Zwei Maschinen können beide Port 3000 verwenden, etwa 10.40.0.10:3000 und 10.40.0.11:3000. Klicken Sie nach dem Verbinden mit der rechten Maustaste auf einen Cloud-Port und wählen Sie URL der privaten Adresse kopieren. Diese Adressen sind privat und keine öffentlichen Freigabelinks." } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": " 团队: 未加载" + "value": "Chaque machine a sa propre IP privée. Deux machines peuvent utiliser le port 3000, par exemple 10.40.0.10:3000 et 10.40.0.11:3000. Après connexion, faites un clic droit sur un port Cloud et choisissez Copier l’URL de l’adresse privée. Ces adresses sont privées, ce ne sont pas des liens publics de partage." } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": " Team: nicht geladen" + "value": "Cada máquina tiene su propia IP privada. Dos máquinas pueden usar el puerto 3000, como 10.40.0.10:3000 y 10.40.0.11:3000. Después de conectar, haz clic derecho en un puerto Cloud y elige Copiar URL de dirección privada. Estas direcciones son privadas, no enlaces públicos para compartir." } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": " équipe: non chargée" + "value": "各マシンには個別のプライベートIPがあります。2台のマシンがどちらもポート3000を使えます(例:10.40.0.10:3000と10.40.0.11:3000)。接続後にCloudポートを右クリックし、「プライベートアドレスURLをコピー」を選びます。これらはプライベートアドレスで、公開共有リンクではありません。" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": " الفريق: لم يتم تحميله" + "value": "每台机器都有自己的私有 IP。两台机器可以同时使用端口 3000,例如 10.40.0.10:3000 和 10.40.0.11:3000。连接后右键点击 Cloud 端口,选择“复制私有地址 URL”。这些地址是私有地址,不是公开分享链接。" } }, - "es": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": " equipo: no cargado" + "value": "每台機器都有自己的私有 IP。兩台機器可以同時使用連接埠 3000,例如 10.40.0.10:3000 和 10.40.0.11:3000。連線後對 Cloud 連接埠按右鍵,選擇「複製私有位址 URL」。這些位址是私有位址,不是公開分享連結。" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": " 團隊: 未載入" + "value": "각 머신에는 고유한 비공개 IP가 있습니다. 두 머신 모두 포트 3000을 사용할 수 있습니다(예: 10.40.0.10:3000 및 10.40.0.11:3000). 연결 후 Cloud 포트를 오른쪽 클릭하고 비공개 주소 URL 복사를 선택하세요. 이 주소는 비공개이며 공개 공유 링크가 아닙니다." } }, - "ko": { + "ar": { "stringUnit": { "state": "translated", - "value": " 팀: 로드되지 않음" + "value": "لكل جهاز عنوان IP خاص به. يمكن لجهازين استخدام المنفذ 3000، مثل 10.40.0.10:3000 و10.40.0.11:3000. بعد الاتصال، انقر بزر الماوس الأيمن على منفذ Cloud واختر نسخ رابط العنوان الخاص. هذه عناوين خاصة وليست روابط مشاركة عامة." } }, "bs": { "stringUnit": { "state": "translated", - "value": " tim: nije učitan" + "value": "Svaka mašina ima svoju privatnu IP adresu. Dvije mašine mogu koristiti port 3000, na primjer 10.40.0.10:3000 i 10.40.0.11:3000. Nakon povezivanja kliknite desnim dugmetom na Cloud port i izaberite Kopiraj URL privatne adrese. Ovo su privatne adrese, a ne javne veze za dijeljenje." } }, "da": { "stringUnit": { "state": "translated", - "value": " team: ikke indlæst" + "value": "Hver maskine har sin egen private IP. To maskiner kan begge bruge port 3000, f.eks. 10.40.0.10:3000 og 10.40.0.11:3000. Højreklik på en Cloud-port efter tilslutning, og vælg Kopiér URL til privat adresse. Adresserne er private, ikke offentlige delelinks." } }, "it": { "stringUnit": { "state": "translated", - "value": " team: non caricato" + "value": "Ogni macchina ha il proprio IP privato. Due macchine possono usare entrambe la porta 3000, ad esempio 10.40.0.10:3000 e 10.40.0.11:3000. Dopo la connessione fai clic destro su una porta Cloud e scegli Copia URL indirizzo privato. Sono indirizzi privati, non link pubblici." } }, "km": { "stringUnit": { "state": "translated", - "value": " ក្រុម៖ មិនទាន់បានផ្ទុក" + "value": "ម៉ាស៊ីននីមួយៗមាន IP ឯកជនផ្ទាល់ខ្លួន។ ម៉ាស៊ីនពីរអាចប្រើច្រក 3000 ដូចគ្នា ឧទាហរណ៍ 10.40.0.10:3000 និង 10.40.0.11:3000។ បន្ទាប់ពីភ្ជាប់ ចុចខាងស្តាំលើច្រក Cloud ហើយជ្រើស ចម្លង URL អាសយដ្ឋានឯកជន។ ទាំងនេះជាអាសយដ្ឋានឯកជន មិនមែនតំណចែករំលែកសាធារណៈទេ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": " team: ikke lastet" + "value": "Hver maskin har sin egen private IP. To maskiner kan bruke port 3000, for eksempel 10.40.0.10:3000 og 10.40.0.11:3000. Høyreklikk en Cloud-port etter tilkobling og velg Kopier URL for privat adresse. Adressene er private, ikke offentlige delingslenker." } }, "pl": { "stringUnit": { "state": "translated", - "value": " zespół: nie załadowano" + "value": "Każda maszyna ma własny prywatny adres IP. Dwie maszyny mogą używać portu 3000, na przykład 10.40.0.10:3000 i 10.40.0.11:3000. Po połączeniu kliknij prawym przyciskiem port Cloud i wybierz Kopiuj URL prywatnego adresu. To prywatne adresy, nie publiczne linki udostępniania." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": " equipe: não carregada" + "value": "Cada máquina tem seu próprio IP privado. Duas máquinas podem usar a porta 3000, por exemplo 10.40.0.10:3000 e 10.40.0.11:3000. Depois de conectar, clique com o botão direito em uma porta Cloud e escolha Copiar URL do endereço privado. Esses endereços são privados, não links públicos de compartilhamento." } }, "ru": { "stringUnit": { "state": "translated", - "value": " команда: не загружена" + "value": "У каждой машины свой частный IP. Две машины могут использовать порт 3000, например 10.40.0.10:3000 и 10.40.0.11:3000. После подключения щёлкните правой кнопкой мыши порт Cloud и выберите Скопировать URL частного адреса. Это частные адреса, а не публичные ссылки." } }, "th": { "stringUnit": { "state": "translated", - "value": " ทีม: ยังไม่ได้โหลด" + "value": "แต่ละเครื่องมี IP ส่วนตัวของตนเอง สองเครื่องสามารถใช้พอร์ต 3000 ได้ เช่น 10.40.0.10:3000 และ 10.40.0.11:3000 หลังเชื่อมต่อ คลิกขวาที่พอร์ต Cloud แล้วเลือกคัดลอก URL ที่อยู่ส่วนตัว ที่อยู่เหล่านี้เป็นส่วนตัว ไม่ใช่ลิงก์สาธารณะ" } }, "tr": { "stringUnit": { "state": "translated", - "value": " ekip: yüklenmedi" + "value": "Her makinenin kendi özel IP’si vardır. İki makine 3000 bağlantı noktasını kullanabilir; örneğin 10.40.0.10:3000 ve 10.40.0.11:3000. Bağlandıktan sonra bir Cloud portuna sağ tıklayın ve Özel Adres URL’sini Kopyala’yı seçin. Bunlar özel adreslerdir, herkese açık paylaşım bağlantıları değildir." } }, "uk": { "stringUnit": { "state": "translated", - "value": " команда: не завантажена" + "value": "Кожна машина має власну приватну IP-адресу. Дві машини можуть використовувати порт 3000, наприклад 10.40.0.10:3000 і 10.40.0.11:3000. Після підключення клацніть правою кнопкою миші порт Cloud і виберіть Копіювати URL приватної адреси. Це приватні адреси, а не публічні посилання для спільного доступу." } } } }, - "cli.auth.status.teamNotLoadedRetrying": { + "cloud.vpn.setup.cancel": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": " team: not loaded (retrying)" + "value": "Cancel" } }, - "ja": { + "de": { "stringUnit": { "state": "translated", - "value": " チーム: 未読み込み(再試行中)" + "value": "Abbrechen" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": " 团队: 未加载(正在重试)" + "value": "Annuler" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": " Team: nicht geladen (wird erneut versucht)" + "value": "Cancelar" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": " équipe: non chargée (nouvel essai)" + "value": "キャンセル" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": " الفريق: لم يتم تحميله (جارٍ إعادة المحاولة)" + "value": "取消" } }, - "es": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": " equipo: no cargado (reintentando)" + "value": "取消" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": " 團隊: 未載入(重試中)" + "value": "취소" } }, - "ko": { + "ar": { "stringUnit": { "state": "translated", - "value": " 팀: 로드되지 않음 (재시도 중)" + "value": "إلغاء" } }, "bs": { "stringUnit": { "state": "translated", - "value": " tim: nije učitan (ponovni pokušaj)" + "value": "Otkaži" } }, "da": { "stringUnit": { "state": "translated", - "value": " team: ikke indlæst (prøver igen)" + "value": "Annuller" } }, "it": { "stringUnit": { "state": "translated", - "value": " team: non caricato (nuovo tentativo)" + "value": "Annulla" } }, "km": { "stringUnit": { "state": "translated", - "value": " ក្រុម៖ មិនទាន់បានផ្ទុក (កំពុងព្យាយាមម្ដងទៀត)" + "value": "បោះបង់" } }, "nb": { "stringUnit": { "state": "translated", - "value": " team: ikke lastet (prøver igjen)" + "value": "Avbryt" } }, "pl": { "stringUnit": { "state": "translated", - "value": " zespół: nie załadowano (ponawianie próby)" + "value": "Anuluj" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": " equipe: não carregada (tentando novamente)" + "value": "Cancelar" } }, "ru": { "stringUnit": { "state": "translated", - "value": " команда: не загружена (повторная попытка)" + "value": "Отменить" } }, "th": { "stringUnit": { "state": "translated", - "value": " ทีม: ยังไม่ได้โหลด (กำลังลองอีกครั้ง)" + "value": "ยกเลิก" } }, "tr": { "stringUnit": { "state": "translated", - "value": " ekip: yüklenmedi (yeniden deneniyor)" + "value": "İptal" } }, "uk": { "stringUnit": { "state": "translated", - "value": " команда: не завантажена (повторна спроба)" + "value": "Скасувати" } } } }, - "mobile.pairing.error.teamUnavailable": { + "cloud.vpn.setup.connect": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Could not load your cmux team. Check your internet connection, then try again." + "value": "Connect Cloud VPN" } }, - "ja": { + "de": { "stringUnit": { "state": "translated", - "value": "cmux チームを読み込めませんでした。インターネット接続を確認してから、もう一度お試しください。" + "value": "Cloud-VPN verbinden" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "无法加载你的 cmux 团队。请检查网络连接,然后重试。" + "value": "Connecter le VPN Cloud" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "Dein cmux-Team konnte nicht geladen werden. Prüfe deine Internetverbindung und versuche es erneut." + "value": "Conectar VPN de Cloud" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Impossible de charger votre équipe cmux. Vérifiez votre connexion Internet, puis réessayez." + "value": "Cloud VPNに接続" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "تعذّر تحميل فريق cmux الخاص بك. تحقّق من اتصالك بالإنترنت، ثم حاول مرة أخرى." + "value": "连接 Cloud VPN" } }, - "es": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "No se pudo cargar tu equipo de cmux. Comprueba tu conexión a internet e inténtalo de nuevo." + "value": "連接 Cloud VPN" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "無法載入你的 cmux 團隊。請檢查網路連線,然後再試一次。" + "value": "Cloud VPN 연결" } }, - "ko": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux 팀을 불러올 수 없습니다. 인터넷 연결을 확인한 후 다시 시도하세요." + "value": "توصيل VPN الخاص بـ Cloud" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Nije moguće učitati vaš cmux tim. Provjerite internetsku vezu i pokušajte ponovo." + "value": "Poveži Cloud VPN" } }, "da": { "stringUnit": { "state": "translated", - "value": "Din cmux-team kunne ikke indlæses. Kontrollér din internetforbindelse, og prøv igen." + "value": "Forbind Cloud VPN" } }, "it": { "stringUnit": { "state": "translated", - "value": "Impossibile caricare il tuo team cmux. Controlla la connessione Internet e riprova." + "value": "Connetti VPN Cloud" } }, "km": { "stringUnit": { "state": "translated", - "value": "មិនអាចផ្ទុកក្រុម cmux របស់អ្នកបានទេ។ សូមពិនិត្យការតភ្ជាប់អ៊ីនធឺណិត ហើយព្យាយាមម្ដងទៀត។" + "value": "ភ្ជាប់ Cloud VPN" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Kunne ikke laste inn cmux-teamet ditt. Kontroller internettilkoblingen, og prøv igjen." + "value": "Koble til Cloud VPN" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Nie udało się załadować Twojego zespołu cmux. Sprawdź połączenie z internetem i spróbuj ponownie." + "value": "Połącz z Cloud VPN" } }, "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." + "value": "Conectar Cloud VPN" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Не удалось загрузить вашу команду cmux. Проверьте подключение к интернету и попробуйте ещё раз." + "value": "Подключить Cloud VPN" } }, "th": { "stringUnit": { "state": "translated", - "value": "ไม่สามารถโหลดทีม cmux ของคุณได้ ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองอีกครั้ง" + "value": "เชื่อมต่อ Cloud VPN" } }, "tr": { "stringUnit": { "state": "translated", - "value": "cmux ekibiniz yüklenemedi. İnternet bağlantınızı kontrol edip tekrar deneyin." + "value": "Cloud VPN’e bağlan" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Не вдалося завантажити вашу команду cmux. Перевірте підключення до інтернету й спробуйте ще раз." + "value": "Підключити Cloud VPN" } } } }, - "command.newPaneAutoLayout.title": { + "cloud.vpn.setup.connected": { "extractionState": "manual", "localizations": { - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "لوحة جديدة (تخطيط تلقائي)" + "value": "Connected" } }, - "bs": { + "de": { "stringUnit": { "state": "translated", - "value": "Novi panel (automatski raspored)" + "value": "Verbunden" } }, - "da": { + "fr": { "stringUnit": { "state": "translated", - "value": "Nyt panel (automatisk layout)" + "value": "Connecté" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "Neuer Bereich (automatisches Layout)" + "value": "Conectada" } }, - "en": { + "ja": { "stringUnit": { "state": "translated", - "value": "New Pane (Auto Layout)" + "value": "接続済み" } }, - "es": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Nuevo panel (diseño automático)" + "value": "已连接" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Nouveau panneau (disposition automatique)" + "value": "已連線" } }, - "it": { + "ko": { "stringUnit": { "state": "translated", - "value": "Nuovo pannello (layout automatico)" + "value": "연결됨" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "新しいペイン(自動レイアウト)" + "value": "متصل" } }, - "ko": { + "bs": { "stringUnit": { "state": "translated", - "value": "새 패널 (자동 레이아웃)" + "value": "Povezano" } }, - "nb": { + "da": { "stringUnit": { "state": "translated", - "value": "Nytt panel (automatisk oppsett)" + "value": "Forbundet" } }, - "pl": { + "it": { "stringUnit": { "state": "translated", - "value": "Nowy panel (układ automatyczny)" + "value": "Connessa" } }, - "pt-BR": { + "km": { "stringUnit": { "state": "translated", - "value": "Novo Painel (Layout Automático)" + "value": "បានភ្ជាប់" } }, - "ru": { + "nb": { "stringUnit": { "state": "translated", - "value": "Новая панель (автоматическая раскладка)" + "value": "Tilkoblet" } }, - "th": { + "pl": { "stringUnit": { "state": "translated", - "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" + "value": "Połączono" } }, - "tr": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Yeni Bölme (Otomatik Düzen)" + "value": "Conectada" } }, - "uk": { + "ru": { "stringUnit": { "state": "translated", - "value": "Нова панель (автоматичне розташування)" + "value": "Подключено" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "新建窗格(自动布局)" + "value": "เชื่อมต่อแล้ว" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "新增窗格(自動佈局)" + "value": "Bağlandı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Підключено" } } } }, - "shortcut.newPaneAutoLayout.label": { + "cloud.vpn.setup.disconnect": { "extractionState": "manual", "localizations": { - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "لوحة جديدة (تخطيط تلقائي)" + "value": "Disconnect" } }, - "bs": { + "de": { "stringUnit": { "state": "translated", - "value": "Novi panel (automatski raspored)" + "value": "Trennen" } }, - "da": { + "fr": { "stringUnit": { "state": "translated", - "value": "Nyt panel (automatisk layout)" + "value": "Déconnecter" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "Neuer Bereich (automatisches Layout)" + "value": "Desconectar" } }, - "en": { + "ja": { "stringUnit": { "state": "translated", - "value": "New Pane (Auto Layout)" + "value": "切断" } }, - "es": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Nuevo panel (diseño automático)" + "value": "断开连接" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Nouveau panneau (disposition automatique)" + "value": "中斷連線" } }, - "it": { + "ko": { "stringUnit": { "state": "translated", - "value": "Nuovo pannello (layout automatico)" + "value": "연결 해제" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "新しいペイン(自動レイアウト)" + "value": "قطع الاتصال" } }, - "ko": { + "bs": { "stringUnit": { "state": "translated", - "value": "새 패널 (자동 레이아웃)" + "value": "Prekini vezu" } }, - "nb": { + "da": { "stringUnit": { "state": "translated", - "value": "Nytt panel (automatisk oppsett)" + "value": "Afbryd" } }, - "pl": { + "it": { "stringUnit": { "state": "translated", - "value": "Nowy panel (układ automatyczny)" + "value": "Disconnetti" } }, - "pt-BR": { + "km": { "stringUnit": { "state": "translated", - "value": "Novo Painel (Layout Automático)" + "value": "ផ្តាច់" } }, - "ru": { + "nb": { "stringUnit": { "state": "translated", - "value": "Новая панель (автоматическая раскладка)" + "value": "Koble fra" } }, - "th": { + "pl": { "stringUnit": { "state": "translated", - "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" + "value": "Rozłącz" } }, - "tr": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Yeni Bölme (Otomatik Düzen)" + "value": "Desconectar" } }, - "uk": { + "ru": { "stringUnit": { "state": "translated", - "value": "Нова панель (автоматичне розташування)" + "value": "Отключить" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "新建窗格(自动布局)" + "value": "ตัดการเชื่อมต่อ" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "新增窗格(自動佈局)" + "value": "Bağlantıyı kes" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Від’єднати" } } } }, - "socket.terminal.notRunning.awaitingRestore": { + "cloud.vpn.setup.entry.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux is still restoring the terminal after it reopened and is checking which agent session to resume. Retry in a few seconds." + "value": "Optional private IP access for other apps" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "لا يزال cmux يستعيد الطرفية بعد إعادة فتحه ويتحقق من جلسة الوكيل التي يجب استئنافها. أعد المحاولة بعد بضع ثوانٍ." + "value": "Optionaler Zugriff auf private IPs für andere Apps" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "cmux stellt das Terminal nach dem erneuten Öffnen noch wieder her und prüft, welche Agent-Sitzung fortgesetzt werden soll. Versuche es in ein paar Sekunden erneut." + "value": "Accès facultatif aux IP privées pour les autres apps" } }, "es": { "stringUnit": { "state": "translated", - "value": "cmux todavía está restaurando la terminal después de volver a abrirse y comprueba qué sesión de agente reanudar. Vuelve a intentarlo en unos segundos." + "value": "Acceso opcional a IP privadas para otras apps" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "cmux restaure encore le terminal après sa réouverture et vérifie quelle session d’agent reprendre. Réessayez dans quelques secondes." + "value": "ほかのアプリからプライベートIPへアクセスする任意の設定" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux は再起動後にターミナルを復元中で、再開するエージェントセッションを確認しています。数秒後に再試行してください。" + "value": "为其他应用提供可选的私有 IP 访问" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux가 다시 열린 후 터미널을 아직 복원하는 중이며 어떤 에이전트 세션을 재개할지 확인하고 있습니다. 몇 초 후에 다시 시도하세요." + "value": "為其他 App 提供選用的私有 IP 存取" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux 重新打开后仍在恢复此终端,并确认要恢复哪个代理会话。请几秒后重试。" + "value": "다른 앱의 비공개 IP 접근을 위한 선택 설정" } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux 重新開啟後仍在還原此終端機,並確認要恢復哪個代理工作階段。請幾秒後再試。" + "value": "وصول اختياري إلى عناوين IP الخاصة للتطبيقات الأخرى" } }, "bs": { "stringUnit": { "state": "translated", - "value": "cmux još vraća terminal nakon ponovnog otvaranja i provjerava koju sesiju agenta treba nastaviti. Pokušajte ponovo za nekoliko sekundi." + "value": "Opcionalni pristup privatnim IP adresama za druge aplikacije" } }, "da": { "stringUnit": { "state": "translated", - "value": "cmux gendanner stadig terminalen efter genåbning og tjekker, hvilken agentsession der skal genoptages. Prøv igen om et par sekunder." + "value": "Valgfri privat IP-adgang for andre apps" } }, "it": { "stringUnit": { "state": "translated", - "value": "cmux sta ancora ripristinando il terminale dopo la riapertura e verifica quale sessione dell’agente riprendere. Riprova tra qualche secondo." + "value": "Accesso facoltativo agli IP privati per altre app" } }, "km": { "stringUnit": { "state": "translated", - "value": "cmux នៅតែកំពុងស្ដារទែមីណាល់ឡើងវិញ បន្ទាប់ពីបើកម្ដងទៀត ហើយកំពុងពិនិត្យមើលថាត្រូវបន្តវគ្គភ្នាក់ងារមួយណា។ សូមព្យាយាមម្ដងទៀតក្នុងរយៈពេលពីរបីវិនាទី។" + "value": "ចូលប្រើ IP ឯកជនសម្រាប់កម្មវិធីផ្សេងៗជាជម្រើស" } }, "nb": { "stringUnit": { "state": "translated", - "value": "cmux gjenoppretter fortsatt terminalen etter at den ble åpnet på nytt, og sjekker hvilken agentøkt som skal gjenopptas. Prøv igjen om noen sekunder." + "value": "Valgfri privat IP-tilgang for andre apper" } }, "pl": { "stringUnit": { "state": "translated", - "value": "cmux wciąż przywraca terminal po ponownym otwarciu i sprawdza, którą sesję agenta wznowić. Spróbuj ponownie za kilka sekund." + "value": "Opcjonalny dostęp innych aplikacji do prywatnych adresów IP" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O cmux ainda está restaurando o terminal após ser reaberto e verificando qual sessão do agente retomar. Tente novamente em alguns segundos." + "value": "Acesso opcional a IPs privados para outros apps" } }, "ru": { "stringUnit": { "state": "translated", - "value": "cmux ещё восстанавливает терминал после повторного открытия и проверяет, какой сеанс агента возобновить. Повторите попытку через несколько секунд." + "value": "Необязательный доступ других приложений к частным IP" } }, "th": { "stringUnit": { "state": "translated", - "value": "cmux ยังคงกู้คืนเทอร์มินัลหลังจากเปิดใหม่ และกำลังตรวจสอบว่าจะทำเซสชันเอเจนต์ใดต่อ ลองอีกครั้งในอีกไม่กี่วินาที" + "value": "การเข้าถึง IP ส่วนตัวสำหรับแอปอื่นแบบไม่บังคับ" } }, "tr": { "stringUnit": { "state": "translated", - "value": "cmux yeniden açıldıktan sonra terminali hâlâ geri yüklüyor ve hangi ajan oturumunun sürdürüleceğini denetliyor. Birkaç saniye sonra yeniden deneyin." + "value": "Diğer uygulamalar için isteğe bağlı özel IP erişimi" } }, "uk": { "stringUnit": { "state": "translated", - "value": "cmux ще відновлює термінал після повторного відкриття та перевіряє, який сеанс агента відновити. Спробуйте ще раз за кілька секунд." + "value": "Необов’язковий доступ інших програм до приватних IP" } } } }, - "socket.terminal.notRunning.closing": { + "cloud.vpn.setup.failed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The terminal is closing and cannot be read." + "value": "Needs attention" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "الطرفية قيد الإغلاق ولا يمكن قراءتها." + "value": "Aktion erforderlich" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Das Terminal wird geschlossen und kann nicht gelesen werden." + "value": "Action requise" } }, "es": { "stringUnit": { "state": "translated", - "value": "La terminal se está cerrando y no se puede leer." + "value": "Requiere atención" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Le terminal est en cours de fermeture et ne peut pas être lu." + "value": "対応が必要" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ターミナルは閉じられているため読み取れません。" + "value": "需要处理" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "터미널이 닫히는 중이라 읽을 수 없습니다." + "value": "需要處理" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "此终端正在关闭,无法读取。" + "value": "조치 필요" } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "此終端機正在關閉,無法讀取。" + "value": "يتطلب إجراءً" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Terminal se zatvara i ne može se pročitati." + "value": "Potrebna pažnja" } }, "da": { "stringUnit": { "state": "translated", - "value": "Terminalen lukkes og kan ikke læses." + "value": "Kræver opmærksomhed" } }, "it": { "stringUnit": { "state": "translated", - "value": "Il terminale si sta chiudendo e non può essere letto." + "value": "Richiede attenzione" } }, "km": { "stringUnit": { "state": "translated", - "value": "ទែមីណាល់កំពុងបិទ ហើយមិនអាចអានបានទេ។" + "value": "ត្រូវការការយកចិត្តទុកដាក់" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Terminalen lukkes og kan ikke leses." + "value": "Krever handling" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Terminal jest zamykany i nie można go odczytać." + "value": "Wymaga uwagi" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O terminal está sendo fechado e não pode ser lido." + "value": "Requer atenção" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Терминал закрывается, и его нельзя прочитать." + "value": "Требуется действие" } }, "th": { "stringUnit": { "state": "translated", - "value": "เทอร์มินัลกำลังปิดและไม่สามารถอ่านได้" + "value": "ต้องดำเนินการ" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Terminal kapanıyor ve okunamıyor." + "value": "İlgilenilmeli" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Термінал закривається, і його не можна прочитати." + "value": "Потрібна увага" } } } }, - "socket.terminal.notRunning.hibernated": { + "cloud.vpn.setup.off": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The terminal is hibernated: cmux suspended its idle agent to save memory. Wake it by showing it (this brings its window to the front), then retry: %@" + "value": "Off" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "الطرفية في وضع السبات: أوقف cmux وكيلها الخامل مؤقتًا لتوفير الذاكرة. أيقظها بعرضها (يؤدي ذلك إلى إحضار نافذتها إلى المقدمة)، ثم أعد المحاولة: %@" + "value": "Aus" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Das Terminal ruht: cmux hat seinen inaktiven Agenten angehalten, um Speicher zu sparen. Wecke es, indem du es anzeigst (dadurch kommt sein Fenster in den Vordergrund), und versuche es dann erneut: %@" + "value": "Désactivé" } }, "es": { "stringUnit": { "state": "translated", - "value": "La terminal está hibernada: cmux suspendió su agente inactivo para ahorrar memoria. Despiértala mostrándola (esto trae su ventana al frente) y vuelve a intentarlo: %@" + "value": "Desactivada" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Le terminal est en veille : cmux a suspendu son agent inactif pour économiser de la mémoire. Réveillez-le en l’affichant (sa fenêtre passe au premier plan), puis réessayez : %@" + "value": "オフ" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ターミナルは休止中です。cmux がメモリ節約のためアイドル状態のエージェントを停止しました。表示すると再開します(ウィンドウが前面に出ます)。その後、再試行してください: %@" + "value": "已关闭" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "터미널이 최대 절전 상태입니다. cmux가 메모리를 절약하려고 유휴 에이전트를 일시 중단했습니다. 터미널을 표시해 깨운 후(창이 앞으로 옵니다) 다시 시도하세요: %@" + "value": "已關閉" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "此终端已休眠:cmux 暂停了其空闲的代理以节省内存。显示它即可唤醒(这会将其窗口带到最前面),然后重试:%@" + "value": "꺼짐" } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "此終端機已休眠:cmux 暫停了其閒置的代理以節省記憶體。顯示它即可喚醒(這會將其視窗帶到最前面),然後再試一次:%@" + "value": "متوقف" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Terminal je u hibernaciji: cmux je pauzirao njegovog neaktivnog agenta radi uštede memorije. Probudite ga tako što ćete ga prikazati (njegov prozor prelazi u prvi plan), pa pokušajte ponovo: %@" + "value": "Isključeno" } }, "da": { "stringUnit": { "state": "translated", - "value": "Terminalen er i dvale: cmux har sat dens inaktive agent på pause for at spare hukommelse. Væk den ved at vise den (dens vindue kommer forrest), og prøv igen: %@" + "value": "Fra" } }, "it": { "stringUnit": { "state": "translated", - "value": "Il terminale è ibernato: cmux ha sospeso il suo agente inattivo per risparmiare memoria. Riattivalo mostrandolo (la sua finestra passa in primo piano), quindi riprova: %@" + "value": "Disattivata" } }, "km": { "stringUnit": { "state": "translated", - "value": "ទែមីណាល់កំពុងសម្រាក៖ cmux បានផ្អាកភ្នាក់ងារដែលទំនេររបស់វា ដើម្បីសន្សំអង្គចងចាំ។ ដាស់វាដោយបង្ហាញវា (បង្អួចរបស់វានឹងមកខាងមុខ) រួចព្យាយាមម្ដងទៀត៖ %@" + "value": "បិទ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Terminalen er i dvale: cmux har satt den inaktive agenten på pause for å spare minne. Vekk den ved å vise den (vinduet kommer i forgrunnen), og prøv igjen: %@" + "value": "Av" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Terminal jest uśpiony: cmux wstrzymał jego bezczynnego agenta, aby oszczędzić pamięć. Wybudź go, wyświetlając go (jego okno przejdzie na pierwszy plan), a potem spróbuj ponownie: %@" + "value": "Wyłączona" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O terminal está hibernado: o cmux suspendeu o agente ocioso para economizar memória. Desperte-o exibindo-o (a janela vem para a frente) e tente novamente: %@" + "value": "Desativada" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Терминал в спящем режиме: cmux приостановил неактивного агента, чтобы сэкономить память. Разбудите его, показав терминал (его окно выйдет на передний план), и повторите попытку: %@" + "value": "Выключено" } }, "th": { "stringUnit": { "state": "translated", - "value": "เทอร์มินัลอยู่ในโหมดพัก: cmux หยุดเอเจนต์ที่ไม่ได้ใช้งานชั่วคราวเพื่อประหยัดหน่วยความจำ ปลุกโดยแสดงเทอร์มินัล (หน้าต่างจะมาอยู่ด้านหน้า) แล้วลองอีกครั้ง: %@" + "value": "ปิด" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Terminal hazırda bekliyor: cmux bellek tasarrufu için boştaki ajanını askıya aldı. Onu göstererek uyandırın (penceresi öne gelir), ardından yeniden deneyin: %@" + "value": "Kapalı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Термінал у режимі сну: cmux призупинив неактивного агента, щоб заощадити пам’ять. Розбудіть його, показавши термінал (його вікно перейде на передній план), і спробуйте ще раз: %@" + "value": "Вимкнено" } } } }, - "socket.terminal.notRunning.hibernatedNoCommand": { + "cloud.vpn.setup.openUnavailable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The terminal is hibernated: cmux suspended its idle agent to save memory. Show the terminal in cmux to wake it, then retry." + "value": "Cloud VPN setup is unavailable" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "الطرفية في وضع السبات: أوقف cmux وكيلها الخامل مؤقتًا لتوفير الذاكرة. اعرض الطرفية في cmux لإيقاظها، ثم أعد المحاولة." + "value": "Cloud-VPN-Einrichtung ist nicht verfügbar" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Das Terminal ruht: cmux hat seinen inaktiven Agenten angehalten, um Speicher zu sparen. Zeige das Terminal in cmux an, um es zu wecken, und versuche es dann erneut." + "value": "La configuration du VPN Cloud est indisponible" } }, "es": { "stringUnit": { "state": "translated", - "value": "La terminal está hibernada: cmux suspendió su agente inactivo para ahorrar memoria. Muestra la terminal en cmux para despertarla y vuelve a intentarlo." + "value": "La configuración de VPN de Cloud no está disponible" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Le terminal est en veille : cmux a suspendu son agent inactif pour économiser de la mémoire. Affichez le terminal dans cmux pour le réveiller, puis réessayez." + "value": "Cloud VPN設定は利用できません" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ターミナルは休止中です。cmux がメモリ節約のためアイドル状態のエージェントを停止しました。cmux でターミナルを表示して再開してから、再試行してください。" + "value": "Cloud VPN 设置不可用" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "터미널이 최대 절전 상태입니다. cmux가 메모리를 절약하려고 유휴 에이전트를 일시 중단했습니다. cmux에서 터미널을 표시해 깨운 후 다시 시도하세요." + "value": "無法使用 Cloud VPN 設定" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "此终端已休眠:cmux 暂停了其空闲的代理以节省内存。请在 cmux 中显示此终端以唤醒它,然后重试。" + "value": "Cloud VPN 설정을 사용할 수 없습니다" } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "此終端機已休眠:cmux 暫停了其閒置的代理以節省記憶體。請在 cmux 中顯示此終端機以喚醒它,然後再試一次。" + "value": "إعداد VPN الخاص بـ Cloud غير متاح" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Terminal je u hibernaciji: cmux je pauzirao njegovog neaktivnog agenta radi uštede memorije. Prikažite terminal u cmuxu da ga probudite, pa pokušajte ponovo." + "value": "Podešavanje Cloud VPN-a nije dostupno" } }, "da": { "stringUnit": { "state": "translated", - "value": "Terminalen er i dvale: cmux har sat dens inaktive agent på pause for at spare hukommelse. Vis terminalen i cmux for at vække den, og prøv igen." + "value": "Cloud VPN-opsætning er ikke tilgængelig" } }, "it": { "stringUnit": { "state": "translated", - "value": "Il terminale è ibernato: cmux ha sospeso il suo agente inattivo per risparmiare memoria. Mostra il terminale in cmux per riattivarlo, quindi riprova." + "value": "La configurazione VPN Cloud non è disponibile" } }, "km": { "stringUnit": { "state": "translated", - "value": "ទែមីណាល់កំពុងសម្រាក៖ cmux បានផ្អាកភ្នាក់ងារដែលទំនេររបស់វា ដើម្បីសន្សំអង្គចងចាំ។ បង្ហាញទែមីណាល់នៅក្នុង cmux ដើម្បីដាស់វា រួចព្យាយាមម្ដងទៀត។" + "value": "ការរៀបចំ Cloud VPN មិនអាចប្រើបាន" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Terminalen er i dvale: cmux har satt den inaktive agenten på pause for å spare minne. Vis terminalen i cmux for å vekke den, og prøv igjen." + "value": "Cloud VPN-oppsett er ikke tilgjengelig" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Terminal jest uśpiony: cmux wstrzymał jego bezczynnego agenta, aby oszczędzić pamięć. Wyświetl terminal w cmux, aby go wybudzić, a potem spróbuj ponownie." + "value": "Konfiguracja Cloud VPN jest niedostępna" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O terminal está hibernado: o cmux suspendeu o agente ocioso para economizar memória. Mostre o terminal no cmux para despertá-lo e tente novamente." + "value": "A configuração do Cloud VPN não está disponível" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Терминал в спящем режиме: cmux приостановил неактивного агента, чтобы сэкономить память. Покажите терминал в cmux, чтобы разбудить его, и повторите попытку." + "value": "Настройка Cloud VPN недоступна" } }, "th": { "stringUnit": { "state": "translated", - "value": "เทอร์มินัลอยู่ในโหมดพัก: cmux หยุดเอเจนต์ที่ไม่ได้ใช้งานชั่วคราวเพื่อประหยัดหน่วยความจำ แสดงเทอร์มินัลใน cmux เพื่อปลุก แล้วลองอีกครั้ง" + "value": "การตั้งค่า Cloud VPN ไม่พร้อมใช้งาน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Terminal hazırda bekliyor: cmux bellek tasarrufu için boştaki ajanını askıya aldı. Uyandırmak için terminali cmux'ta gösterin, ardından yeniden deneyin." + "value": "Cloud VPN kurulumu kullanılamıyor" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Термінал у режимі сну: cmux призупинив неактивного агента, щоб заощадити пам’ять. Покажіть термінал у cmux, щоб розбудити його, і спробуйте ще раз." + "value": "Налаштування Cloud VPN недоступне" } } } }, - "socket.terminal.notRunning.starting": { + "cloud.vpn.setup.permission.body": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The terminal has not started yet. Retry in a few seconds." + "value": "The VPN software is included in cmux. You do not need another app. macOS may require your password or Touch ID during approval." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "لم تبدأ الطرفية بعد. أعد المحاولة بعد بضع ثوانٍ." + "value": "Die VPN-Software ist in cmux enthalten. Sie brauchen keine weitere App. macOS kann bei der Genehmigung Ihr Passwort oder Touch ID verlangen." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Das Terminal wurde noch nicht gestartet. Versuche es in ein paar Sekunden erneut." + "value": "Le logiciel VPN est inclus dans cmux. Aucune autre app n’est nécessaire. macOS peut demander votre mot de passe ou Touch ID lors de l’autorisation." } }, "es": { "stringUnit": { "state": "translated", - "value": "La terminal aún no se ha iniciado. Vuelve a intentarlo en unos segundos." + "value": "El software VPN está incluido en cmux. No necesitas otra app. macOS puede solicitar tu contraseña o Touch ID durante la autorización." } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Le terminal n’a pas encore démarré. Réessayez dans quelques secondes." + "value": "VPNソフトウェアはcmuxに含まれています。別のアプリは不要です。承認時にmacOSがパスワードまたはTouch IDを求めることがあります。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ターミナルはまだ起動していません。数秒後に再試行してください。" + "value": "cmux 已包含 VPN 软件,无需其他应用。批准时,macOS 可能要求您输入密码或使用触控 ID。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "터미널이 아직 시작되지 않았습니다. 몇 초 후에 다시 시도하세요." + "value": "cmux 已包含 VPN 軟體,不需要其他 App。核准時,macOS 可能要求您輸入密碼或使用 Touch ID。" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "此终端尚未启动。请几秒后重试。" + "value": "VPN 소프트웨어는 cmux에 포함되어 있습니다. 다른 앱은 필요하지 않습니다. 승인 시 macOS에서 암호 또는 Touch ID를 요청할 수 있습니다." } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "此終端機尚未啟動。請幾秒後再試。" + "value": "برنامج VPN مضمن في cmux. لا تحتاج إلى تطبيق آخر. قد يطلب macOS كلمة المرور أو Touch ID أثناء الموافقة." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Terminal se još nije pokrenuo. Pokušajte ponovo za nekoliko sekundi." + "value": "VPN softver je uključen u cmux. Nije potrebna druga aplikacija. macOS može zatražiti lozinku ili Touch ID tokom odobravanja." } }, "da": { "stringUnit": { "state": "translated", - "value": "Terminalen er ikke startet endnu. Prøv igen om et par sekunder." + "value": "VPN-softwaren er inkluderet i cmux. Du behøver ingen anden app. macOS kan bede om din adgangskode eller Touch ID under godkendelsen." } }, "it": { "stringUnit": { "state": "translated", - "value": "Il terminale non è ancora stato avviato. Riprova tra qualche secondo." + "value": "Il software VPN è incluso in cmux. Non serve un’altra app. macOS potrebbe chiedere la password o Touch ID durante l’autorizzazione." } }, "km": { "stringUnit": { "state": "translated", - "value": "ទែមីណាល់មិនទាន់បានចាប់ផ្តើមនៅឡើយទេ។ សូមព្យាយាមម្ដងទៀតក្នុងរយៈពេលពីរបីវិនាទី។" + "value": "កម្មវិធី VPN មាននៅក្នុង cmux។ អ្នកមិនត្រូវការកម្មវិធីផ្សេងទៀតទេ។ macOS អាចស្នើពាក្យសម្ងាត់ ឬ Touch ID ក្នុងពេលអនុម័ត។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Terminalen har ikke startet ennå. Prøv igjen om noen sekunder." + "value": "VPN-programvaren er inkludert i cmux. Du trenger ingen annen app. macOS kan be om passord eller Touch ID under godkjenningen." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Terminal jeszcze się nie uruchomił. Spróbuj ponownie za kilka sekund." + "value": "Oprogramowanie VPN jest zawarte w cmux. Nie potrzebujesz innej aplikacji. macOS może poprosić o hasło lub Touch ID podczas zatwierdzania." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O terminal ainda não foi iniciado. Tente novamente em alguns segundos." + "value": "O software VPN vem incluído no cmux. Você não precisa de outro app. O macOS pode pedir sua senha ou Touch ID durante a aprovação." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Терминал ещё не запущен. Повторите попытку через несколько секунд." + "value": "VPN-программа уже входит в cmux. Другие приложения не нужны. Во время подтверждения macOS может запросить пароль или Touch ID." } }, "th": { "stringUnit": { "state": "translated", - "value": "เทอร์มินัลยังไม่ได้เริ่มทำงาน ลองอีกครั้งในอีกไม่กี่วินาที" + "value": "ซอฟต์แวร์ VPN รวมอยู่ใน cmux แล้ว ไม่ต้องใช้แอปอื่น macOS อาจขอรหัสผ่านหรือ Touch ID ระหว่างการอนุมัติ" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Terminal henüz başlamadı. Birkaç saniye sonra yeniden deneyin." + "value": "VPN yazılımı cmux içinde bulunur. Başka bir uygulama gerekmez. macOS onay sırasında parolanızı veya Touch ID’yi isteyebilir." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Термінал ще не запущено. Спробуйте ще раз за кілька секунд." + "value": "Програмне забезпечення VPN входить до cmux. Інша програма не потрібна. Під час схвалення macOS може попросити пароль або Touch ID." } } } }, - "cli.restoreSession.help": { + "cloud.vpn.setup.status.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nReopen the previous saved cmux session.\n\nIf the app is already running, this restores the last saved session into the current app.\nIf the app is not running, this launches cmux and lets startup restore reopen the saved session.\n\nEach cmux install (stable, nightly, rc, staging, tagged debug builds) saves its own session.\n--from Reopen another install's saved session in this running cmux, for example\n after trying nightly and switching back to stable. The session opens as\n additional windows; the other install's saved file is only read.\n--from Reopen a session file written by --export.\n--export Write this cmux's saved session to a file. Pass --force to replace an\n existing file.\n\n--from and --export require cmux to be running." + "value": "Private network status" } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nDie zuletzt gespeicherte cmux-Sitzung erneut öffnen.\n\nLäuft die App bereits, wird die zuletzt gespeicherte Sitzung in der aktuellen App wiederhergestellt.\nLäuft die App nicht, wird cmux gestartet und die gespeicherte Sitzung beim Start wiederhergestellt.\n\nJede cmux-Installation (stable, nightly, rc, staging, getaggte Debug-Builds) speichert ihre eigene Sitzung.\n--from Die gespeicherte Sitzung einer anderen Installation in diesem laufenden cmux öffnen,\n z. B. nachdem Sie nightly ausprobiert haben und zu stable zurückkehren. Die Sitzung\n öffnet sich in zusätzlichen Fenstern; die Datei der anderen Installation wird nur gelesen.\n--from Eine mit --export geschriebene Sitzungsdatei öffnen.\n--export Die gespeicherte Sitzung dieses cmux in eine Datei schreiben. Mit --force wird eine\n vorhandene Datei ersetzt.\n\n--from und --export setzen ein laufendes cmux voraus." + "value": "Status des privaten Netzwerks" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nRouvrir la dernière session cmux enregistrée.\n\nSi l'app est déjà ouverte, la dernière session enregistrée est restaurée dans l'app actuelle.\nSi l'app n'est pas ouverte, cmux est lancé et la restauration au démarrage rouvre la session enregistrée.\n\nChaque installation de cmux (stable, nightly, rc, staging, builds debug avec tag) enregistre sa propre session.\n--from Rouvrir la session enregistrée d'une autre installation dans ce cmux en cours, par exemple\n après avoir essayé nightly puis être revenu à stable. La session s'ouvre dans des fenêtres\n supplémentaires ; le fichier de l'autre installation est seulement lu.\n--from Rouvrir un fichier de session écrit par --export.\n--export Écrire la session enregistrée de ce cmux dans un fichier. Ajoutez --force pour remplacer\n un fichier existant.\n\n--from et --export nécessitent que cmux soit ouvert." + "value": "État du réseau privé" } }, "es": { "stringUnit": { "state": "translated", - "value": "Uso: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nVuelve a abrir la última sesión guardada de cmux.\n\nSi la app ya está abierta, restaura la última sesión guardada en la app actual.\nSi la app no está abierta, inicia cmux y deja que la restauración de inicio vuelva a abrir la sesión guardada.\n\nCada instalación de cmux (stable, nightly, rc, staging, builds de depuración con etiqueta) guarda su propia sesión.\n--from Abre en este cmux la sesión guardada de otra instalación, por ejemplo después de\n probar nightly y volver a stable. La sesión se abre en ventanas adicionales; el archivo\n de la otra instalación solo se lee.\n--from Abre un archivo de sesión escrito con --export.\n--export Escribe en un archivo la sesión guardada de este cmux. Usa --force para reemplazar un\n archivo existente.\n\n--from y --export requieren que cmux esté abierto." + "value": "Estado de la red privada" } }, "ja": { "stringUnit": { "state": "translated", - "value": "使い方: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n前回保存した cmux セッションを再度開きます。\n\nアプリがすでに起動している場合は、最後に保存したセッションを現在のアプリに復元します。\nアプリが起動していない場合は、cmux を起動し、起動時の復元で保存済みセッションを開きます。\n\ncmux の各インストール(stable、nightly、rc、staging、タグ付き debug ビルド)はそれぞれ独自のセッションを保存します。\n--from 別のインストールで保存したセッションを、起動中のこの cmux で開きます。たとえば nightly を\n 試したあと stable に戻る場合に使います。セッションは追加のウインドウとして開き、別の\n インストールのファイルは読み取るだけです。\n--from --export で書き出したセッションファイルを開きます。\n--export この cmux の保存済みセッションをファイルに書き出します。既存のファイルを置き換えるには\n --force を指定します。\n\n--from と --export を使うには cmux が起動している必要があります。" + "value": "プライベートネットワークの状態" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nأعد فتح آخر جلسة cmux محفوظة.\n\nإذا كان التطبيق يعمل بالفعل، فستُستعاد آخر جلسة محفوظة في التطبيق الحالي.\nإذا لم يكن التطبيق يعمل، فسيُشغَّل cmux وتعيد استعادة بدء التشغيل فتح الجلسة المحفوظة.\n\nيحفظ كل تثبيت من cmux (stable وnightly وrc وstaging وإصدارات debug الموسومة) جلسته الخاصة.\n--from افتح الجلسة المحفوظة لتثبيت آخر في cmux الذي يعمل الآن، مثلًا\n بعد تجربة nightly والعودة إلى stable. تُفتح الجلسة في\n نوافذ إضافية، ويُقرأ ملف التثبيت الآخر فقط.\n--from افتح ملف جلسة كتبه الأمر --export.\n--export اكتب الجلسة المحفوظة لهذا cmux في ملف. استخدم --force لاستبدال\n ملف موجود.\n\nيتطلب --from و--export أن يكون cmux قيد التشغيل." + "value": "私有网络状态" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n重新開啟上次儲存的 cmux 工作階段。\n\n如果 App 已在執行,會將上次儲存的工作階段還原到目前的 App 中。\n如果 App 未執行,會啟動 cmux,並由啟動還原重新開啟已儲存的工作階段。\n\n每個 cmux 安裝(stable、nightly、rc、staging、帶標籤的 debug 建置)都會儲存自己的工作階段。\n--from 在執行中的 cmux 開啟另一個安裝儲存的工作階段,例如\n 試用 nightly 後切回 stable。工作階段會以\n 額外視窗開啟;另一個安裝的檔案只會被讀取。\n--from 開啟由 --export 寫出的工作階段檔案。\n--export 將此 cmux 儲存的工作階段寫入檔案。使用 --force 可取代\n 既有檔案。\n\n--from 和 --export 需要 cmux 正在執行。" + "value": "私有網路狀態" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "用法: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n重新打开上次保存的 cmux 会话。\n\n如果应用已在运行,会将上次保存的会话恢复到当前应用中。\n如果应用未运行,会启动 cmux,并由启动恢复重新打开保存的会话。\n\n每个 cmux 安装(stable、nightly、rc、staging、带标签的 debug 构建)都会保存自己的会话。\n--from 在正在运行的 cmux 中打开另一个安装保存的会话,例如\n 试用 nightly 后切回 stable。会话会以\n 额外窗口打开;另一个安装的会话文件只会被读取。\n--from 打开由 --export 写出的会话文件。\n--export 将此 cmux 保存的会话写入文件。使用 --force 可替换\n 已有文件。\n\n--from 和 --export 需要 cmux 正在运行。" + "value": "비공개 네트워크 상태" } }, - "ko": { + "ar": { "stringUnit": { "state": "translated", - "value": "사용법: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n마지막으로 저장된 cmux 세션을 다시 엽니다.\n\n앱이 이미 실행 중이면 마지막으로 저장된 세션을 현재 앱에 복원합니다.\n앱이 실행 중이 아니면 cmux를 실행하고 시작 복원이 저장된 세션을 다시 열도록 합니다.\n\n각 cmux 설치(stable, nightly, rc, staging, 태그가 지정된 debug 빌드)는 자체 세션을 저장합니다.\n--from 다른 설치에 저장된 세션을 실행 중인 이 cmux에서 엽니다. 예를 들어\n nightly를 사용해 본 뒤 stable로 돌아올 때 사용합니다. 세션은\n 추가 창으로 열리며, 다른 설치의 파일은 읽기만 합니다.\n--from --export로 작성한 세션 파일을 엽니다.\n--export 이 cmux에 저장된 세션을 파일로 씁니다. 기존 파일을 바꾸려면\n --force를 사용하세요.\n\n--from과 --export를 사용하려면 cmux가 실행 중이어야 합니다." + "value": "حالة الشبكة الخاصة" } - } - } - }, - "session.export.error.destinationExists": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "%@ already exists. Pass --force to replace it." + "value": "Status privatne mreže" } }, - "ar": { + "da": { "stringUnit": { "state": "translated", - "value": "%@ موجود بالفعل. استخدم --force لاستبداله." + "value": "Status for privat netværk" } }, - "de": { + "it": { "stringUnit": { "state": "translated", - "value": "%@ ist bereits vorhanden. Verwenden Sie --force, um die Datei zu ersetzen." + "value": "Stato della rete privata" } }, - "es": { + "km": { "stringUnit": { "state": "translated", - "value": "%@ ya existe. Usa --force para reemplazarlo." + "value": "ស្ថានភាពបណ្តាញឯកជន" } }, - "fr": { + "nb": { "stringUnit": { "state": "translated", - "value": "%@ existe déjà. Ajoutez --force pour le remplacer." + "value": "Status for privat nettverk" } }, - "ja": { + "pl": { "stringUnit": { "state": "translated", - "value": "%@ はすでに存在します。置き換えるには --force を指定してください。" + "value": "Stan sieci prywatnej" } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "%@ 파일이 이미 있습니다. 바꾸려면 --force를 사용하세요." + "value": "Status da rede privada" } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "%@ 已存在。使用 --force 以替换它。" + "value": "Состояние частной сети" } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "%@ 已存在。使用 --force 以取代它。" + "value": "สถานะเครือข่ายส่วนตัว" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Özel ağ durumu" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Стан приватної мережі" } } } }, - "session.export.error.destinationIsLive": { + "cloud.vpn.setup.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%@ is this cmux's own session file. Choose another path." + "value": "Cloud VPN" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "%@ هو ملف الجلسة الخاص بهذا الـ cmux. اختر مسارًا آخر." + "value": "Cloud-VPN" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "%@ ist die eigene Sitzungsdatei dieses cmux. Wählen Sie einen anderen Pfad." + "value": "VPN Cloud" } }, "es": { "stringUnit": { "state": "translated", - "value": "%@ es el archivo de sesión de este cmux. Elige otra ruta." + "value": "VPN de Cloud" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "%@ est le fichier de session de ce cmux. Choisissez un autre chemin." + "value": "クラウドVPN" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%@ はこの cmux 自身のセッションファイルです。別のパスを指定してください。" + "value": "云端 VPN" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端 VPN" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@ 파일은 이 cmux의 세션 파일입니다. 다른 경로를 선택하세요." + "value": "클라우드 VPN" } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "%@ 是此 cmux 自己的会话文件。请选择其他路径。" + "value": "شبكة Cloud الخاصة" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "%@ 是此 cmux 本身的工作階段檔案。請選擇其他路徑。" + "value": "Cloud VPN" } - } - } - }, - "session.export.error.noSnapshot": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "cmux has not saved a session yet. Try again in a few seconds." + "value": "Cloud VPN" } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "لم يحفظ cmux أي جلسة بعد. حاول مرة أخرى بعد بضع ثوانٍ." + "value": "VPN Cloud" } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "cmux hat noch keine Sitzung gespeichert. Versuchen Sie es in ein paar Sekunden erneut." + "value": "Cloud VPN" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "cmux aún no ha guardado ninguna sesión. Inténtalo de nuevo en unos segundos." + "value": "Cloud VPN" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "cmux n'a pas encore enregistré de session. Réessayez dans quelques secondes." + "value": "Cloud VPN" } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "cmux はまだセッションを保存していません。数秒後にもう一度お試しください。" + "value": "Cloud VPN" } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "cmux가 아직 세션을 저장하지 않았습니다. 몇 초 후에 다시 시도하세요." + "value": "Cloud VPN" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "cmux 尚未保存会话。请几秒后再试。" + "value": "Cloud VPN" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "cmux 尚未儲存工作階段。請過幾秒再試一次。" + "value": "Cloud VPN" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Cloud VPN" } } } }, - "session.export.error.writeFailed": { + "cloud.vpn.setup.unavailable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Could not write %@." + "value": "This copy of cmux does not include a signed VPN extension. Use a cmux release with Cloud VPN support. Cloud terminals, Ports, and Desktop remain available without the VPN." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "تعذّرت كتابة %@." + "value": "Diese cmux-Version enthält keine signierte VPN-Erweiterung. Verwenden Sie eine cmux-Version mit Cloud-VPN-Unterstützung. Cloud-Terminals, Ports und Desktop bleiben ohne VPN verfügbar." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "%@ konnte nicht geschrieben werden." + "value": "Cette copie de cmux ne contient pas d’extension VPN signée. Utilisez une version de cmux qui prend en charge le VPN Cloud. Les terminaux Cloud, Ports et Bureau restent disponibles sans VPN." } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo escribir %@." + "value": "Esta copia de cmux no incluye una extensión VPN firmada. Usa una versión de cmux compatible con VPN de Cloud. Los terminales Cloud, Puertos y Escritorio siguen disponibles sin VPN." } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Impossible d'écrire %@." + "value": "このcmuxには署名済みのVPN機能拡張が含まれていません。Cloud VPN対応のcmuxリリースを使用してください。Cloudのターミナル、ポート、デスクトップはVPNなしでも使えます。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%@ を書き込めませんでした。" + "value": "此 cmux 副本未包含已签名的 VPN 扩展。请使用支持 Cloud VPN 的 cmux 版本。Cloud 终端、端口和桌面无需 VPN 仍可使用。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此 cmux 副本未包含已簽署的 VPN 延伸功能。請使用支援 Cloud VPN 的 cmux 版本。Cloud 終端機、連接埠和桌面不需要 VPN 仍可使用。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@ 파일을 쓸 수 없습니다." + "value": "이 cmux에는 서명된 VPN 확장이 포함되어 있지 않습니다. Cloud VPN을 지원하는 cmux 릴리스를 사용하세요. Cloud 터미널, 포트 및 데스크탑은 VPN 없이도 사용할 수 있습니다." } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "无法写入 %@。" + "value": "لا تتضمن هذه النسخة من cmux امتداد VPN موقعًا. استخدم إصدار cmux يدعم VPN الخاص بـ Cloud. تظل طرفيات Cloud والمنافذ وسطح المكتب متاحة بدون VPN." } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "無法寫入 %@。" + "value": "Ova kopija cmux-a nema potpisani VPN dodatak. Koristite cmux izdanje sa Cloud VPN podrškom. Cloud terminali, portovi i radna površina ostaju dostupni bez VPN-a." } - } - } - }, - "session.import.error.fileNotFound": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "No saved cmux session at %@." + "value": "Denne cmux-kopi indeholder ikke en signeret VPN-udvidelse. Brug en cmux-udgave med Cloud VPN-understøttelse. Cloud-terminaler, porte og skrivebord er stadig tilgængelige uden VPN." } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "لا توجد جلسة cmux محفوظة في %@." + "value": "Questa copia di cmux non include un’estensione VPN firmata. Usa una versione di cmux con supporto Cloud VPN. Terminali Cloud, porte e desktop restano disponibili senza VPN." } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Unter %@ gibt es keine gespeicherte cmux-Sitzung." + "value": "cmux ច្បាប់នេះមិនមានផ្នែកបន្ថែម VPN ដែលបានចុះហត្ថលេខាទេ។ ប្រើកំណែ cmux ដែលគាំទ្រ Cloud VPN។ Terminal, Ports និង Desktop របស់ Cloud នៅតែប្រើបានដោយគ្មាន VPN។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "No hay ninguna sesión de cmux guardada en %@." + "value": "Denne cmux-kopien har ingen signert VPN-utvidelse. Bruk en cmux-versjon med Cloud VPN-støtte. Cloud-terminaler, porter og skrivebord er tilgjengelige uten VPN." } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Aucune session cmux enregistrée à l'emplacement %@." + "value": "Ta kopia cmux nie zawiera podpisanego rozszerzenia VPN. Użyj wydania cmux z obsługą Cloud VPN. Terminale Cloud, porty i pulpit pozostają dostępne bez VPN." } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "%@ に保存された cmux セッションはありません。" + "value": "Esta cópia do cmux não inclui uma extensão VPN assinada. Use uma versão do cmux com suporte a Cloud VPN. Terminais Cloud, portas e área de trabalho continuam disponíveis sem VPN." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "%@ 위치에 저장된 cmux 세션이 없습니다." + "value": "В этой копии cmux нет подписанного VPN-расширения. Используйте выпуск cmux с поддержкой Cloud VPN. Cloud-терминалы, порты и рабочий стол доступны без VPN." } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "%@ 没有已保存的 cmux 会话。" + "value": "cmux รุ่นนี้ไม่มีส่วนขยาย VPN ที่ลงนามแล้ว ใช้รุ่น cmux ที่รองรับ Cloud VPN เทอร์มินัล Cloud พอร์ต และเดสก์ท็อปยังใช้ได้โดยไม่ต้องมี VPN" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "%@ 沒有已儲存的 cmux 工作階段。" + "value": "Bu cmux kopyasında imzalı VPN uzantısı bulunmuyor. Cloud VPN destekli bir cmux sürümü kullanın. Cloud terminalleri, portlar ve masaüstü VPN olmadan da kullanılabilir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ця копія cmux не містить підписаного VPN-розширення. Використовуйте випуск cmux із підтримкою Cloud VPN. Cloud-термінали, порти й робочий стіл доступні без VPN." } } } }, - "session.import.error.newerSchema": { + "cloud.vpn.setup.waiting": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@ was saved by a newer cmux (session format %2$@, this cmux reads %3$@). Update cmux to import it." + "value": "Waiting" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "حُفظ %1$@ بواسطة إصدار أحدث من cmux (تنسيق الجلسة %2$@، ويقرأ هذا الـ cmux التنسيق %3$@). حدّث cmux لاستيراده." + "value": "Warten" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$@ wurde von einem neueren cmux gespeichert (Sitzungsformat %2$@, dieses cmux liest %3$@). Aktualisieren Sie cmux, um die Datei zu importieren." + "value": "En attente" } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@ se guardó con un cmux más reciente (formato de sesión %2$@; este cmux lee el %3$@). Actualiza cmux para importarlo." + "value": "En espera" } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "%1$@ a été enregistré par une version plus récente de cmux (format de session %2$@, ce cmux lit le format %3$@). Mettez cmux à jour pour l'importer." + "value": "待機中" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@ は新しい cmux で保存されています(セッション形式 %2$@、この cmux が読める形式は %3$@)。読み込むには cmux をアップデートしてください。" + "value": "等待中" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "等待中" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@ 파일은 더 새로운 cmux에서 저장되었습니다(세션 형식 %2$@, 이 cmux는 %3$@ 형식을 읽음). 가져오려면 cmux를 업데이트하세요." + "value": "대기 중" } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "%1$@ 由较新版本的 cmux 保存(会话格式 %2$@,此 cmux 读取 %3$@)。请更新 cmux 后再导入。" + "value": "قيد الانتظار" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "%1$@ 是由較新版本的 cmux 儲存(工作階段格式 %2$@,此 cmux 讀取 %3$@)。請更新 cmux 後再匯入。" + "value": "Čekanje" } - } - } - }, - "session.import.error.noWindows": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "%@ has no windows to restore." + "value": "Venter" } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "لا يحتوي %@ على نوافذ لاستعادتها." + "value": "In attesa" } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "%@ enthält keine Fenster zum Wiederherstellen." + "value": "កំពុងរង់ចាំ" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "%@ no tiene ventanas que restaurar." + "value": "Venter" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "%@ ne contient aucune fenêtre à restaurer." + "value": "Oczekiwanie" } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "%@ には復元できるウインドウがありません。" + "value": "Aguardando" } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "%@ 파일에 복원할 창이 없습니다." + "value": "Ожидание" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "%@ 没有可恢复的窗口。" + "value": "กำลังรอ" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "%@ 沒有可還原的視窗。" + "value": "Bekliyor" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Очікування" } } } }, - "session.import.error.notASnapshot": { + "cloudTree.ports.action.wake": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%@ is not a cmux session snapshot." + "value": "Wake Machine" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "%@ ليس لقطة جلسة cmux." + "value": "Maschine aktivieren" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "%@ ist keine cmux-Sitzungsdatei." + "value": "Réveiller la machine" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إيقاظ الجهاز" } }, "es": { "stringUnit": { "state": "translated", - "value": "%@ no es una instantánea de sesión de cmux." + "value": "Activar máquina" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%@ n'est pas un instantané de session cmux." + "value": "喚醒機器" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%@ は cmux のセッションスナップショットではありません。" + "value": "唤醒机器" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@ 파일은 cmux 세션 스냅샷이 아닙니다." + "value": "머신 깨우기" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "%@ 不是 cmux 会话快照。" + "value": "マシンを起動" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "%@ 不是 cmux 工作階段快照。" + "value": "Probudi mašinu" } - } - } - }, - "session.import.error.nothingRestored": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Nothing in %@ could be reopened." + "value": "Væk maskine" } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "تعذّر إعادة فتح أي شيء من %@." + "value": "Riattiva macchina" } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Aus %@ konnte nichts wieder geöffnet werden." + "value": "ដាស់ម៉ាស៊ីន" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "No se pudo volver a abrir nada de %@." + "value": "Vekk maskin" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Aucun élément de %@ n'a pu être rouvert." + "value": "Wybudź maszynę" } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "%@ から再度開けるものはありませんでした。" + "value": "Despertar máquina" } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "%@ 파일에서 다시 열 수 있는 항목이 없습니다." + "value": "Разбудить машину" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "无法重新打开 %@ 中的任何内容。" + "value": "ปลุกเครื่อง" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "無法重新開啟 %@ 中的任何內容。" + "value": "Makineyi uyandır" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розбудити машину" } } } }, - "session.import.error.olderSchema": { + "cloudTree.ports.asleep.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@ uses an older session format (%2$@) that this cmux no longer reads (%3$@)." + "value": "Wake the machine to check its services. cmux’s in-app forwarding does not need Cloud VPN." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "يستخدم %1$@ تنسيق جلسة أقدم (%2$@) لم يعد هذا الـ cmux يقرؤه (%3$@)." + "value": "Aktivieren Sie die Maschine, um ihre Dienste zu prüfen. Die Weiterleitung in cmux benötigt kein Cloud-VPN." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$@ verwendet ein älteres Sitzungsformat (%2$@), das dieses cmux nicht mehr liest (%3$@)." + "value": "Réveillez la machine pour vérifier ses services. Le transfert intégré à cmux ne nécessite pas de VPN Cloud." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أيقظ الجهاز لفحص خدماته. لا تحتاج إعادة التوجيه داخل cmux إلى Cloud VPN." } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@ usa un formato de sesión anterior (%2$@) que este cmux ya no lee (%3$@)." + "value": "Activa la máquina para comprobar sus servicios. El reenvío integrado de cmux no necesita Cloud VPN." } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@ utilise un ancien format de session (%2$@) que ce cmux ne lit plus (%3$@)." + "value": "喚醒機器以檢查其服務。cmux 的應用程式內轉送不需要 Cloud VPN。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@ はこの cmux が読み込まなくなった古いセッション形式(%2$@)を使用しています(%3$@)。" + "value": "唤醒机器以检查其服务。cmux 的应用内转发无需 Cloud VPN。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@ 파일은 이 cmux가 더 이상 읽지 않는 이전 세션 형식(%2$@)을 사용합니다(%3$@)." + "value": "머신을 깨워 서비스를 확인하세요. cmux 앱 내 전달에는 Cloud VPN이 필요하지 않습니다." } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "%1$@ 使用较旧的会话格式(%2$@),此 cmux 已不再读取(%3$@)。" + "value": "マシンを起動してサービスを確認してください。cmux のアプリ内転送に Cloud VPN は不要です。" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "%1$@ 使用較舊的工作階段格式(%2$@),此 cmux 已不再讀取(%3$@)。" + "value": "Probudite mašinu da provjerite njene servise. Prosljeđivanje unutar cmux-a ne treba Cloud VPN." } - } - } - }, - "session.import.error.unknownChannel": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Unknown cmux channel \"%@\". Use stable, nightly, rc, staging, debug:, or a path to a session file." + "value": "Væk maskinen for at kontrollere dens tjenester. Videresendelse i cmux kræver ikke Cloud VPN." } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "قناة cmux غير معروفة \"%@\". استخدم stable أو nightly أو rc أو staging أو debug: أو مسار ملف جلسة." + "value": "Riattiva la macchina per controllarne i servizi. L’inoltro all’interno di cmux non richiede la VPN Cloud." } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Unbekannter cmux-Kanal \"%@\". Verwenden Sie stable, nightly, rc, staging, debug: oder einen Pfad zu einer Sitzungsdatei." + "value": "ដាស់ម៉ាស៊ីនដើម្បីពិនិត្យសេវាកម្មរបស់វា។ ការបញ្ជូនបន្តនៅក្នុង cmux មិនត្រូវការ Cloud VPN ទេ។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Canal de cmux desconocido \"%@\". Usa stable, nightly, rc, staging, debug: o la ruta de un archivo de sesión." + "value": "Vekk maskinen for å sjekke tjenestene. Videresending i cmux trenger ikke Cloud VPN." } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Canal cmux inconnu « %@ ». Utilisez stable, nightly, rc, staging, debug: ou le chemin d'un fichier de session." + "value": "Wybudź maszynę, aby sprawdzić jej usługi. Przekierowywanie w cmux nie wymaga Cloud VPN." } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "不明な cmux チャンネル「%@」です。stable、nightly、rc、staging、debug:、またはセッションファイルのパスを指定してください。" + "value": "Desperte a máquina para verificar os serviços. O encaminhamento dentro do cmux não precisa do Cloud VPN." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "알 수 없는 cmux 채널 \"%@\"입니다. stable, nightly, rc, staging, debug: 또는 세션 파일 경로를 사용하세요." + "value": "Разбудите машину, чтобы проверить её службы. Перенаправлению внутри cmux Cloud VPN не нужен." } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "未知的 cmux 渠道“%@”。请使用 stable、nightly、rc、staging、debug: 或会话文件路径。" + "value": "ปลุกเครื่องเพื่อตรวจสอบบริการ การส่งต่อภายใน cmux ไม่ต้องใช้ Cloud VPN" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "未知的 cmux 通道「%@」。請使用 stable、nightly、rc、staging、debug: 或工作階段檔案路徑。" + "value": "Hizmetlerini denetlemek için makineyi uyandırın. cmux içi yönlendirme Cloud VPN gerektirmez." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розбудіть машину, щоб перевірити її служби. Перенаправлення всередині cmux не потребує Cloud VPN." } } } }, - "session.import.error.unreadable": { + "cloudTree.ports.available.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Could not read %@." + "value": "Listening services" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "تعذّرت قراءة %@." + "value": "Lauschende Dienste" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "%@ konnte nicht gelesen werden." + "value": "Services en écoute" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الخدمات المستمعة" } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo leer %@." + "value": "Servicios en escucha" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Impossible de lire %@." + "value": "監聽中的服務" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%@ を読み込めませんでした。" + "value": "监听中的服务" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@ 파일을 읽을 수 없습니다." + "value": "수신 대기 서비스" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "无法读取 %@。" + "value": "待ち受け中のサービス" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "無法讀取 %@。" + "value": "Servisi koji slušaju" } - } - } - }, - "session.import.error.liveSnapshot": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "%@ is the session this cmux is saving right now. Run cmux restore-session without --from to reopen the previous launch." + "value": "Lyttende tjenester" } }, - "de": { + "it": { "stringUnit": { "state": "translated", - "value": "%@ ist die Sitzung, die dieses cmux gerade speichert. Führen Sie cmux restore-session ohne --from aus, um den vorherigen Start wieder zu öffnen." + "value": "Servizi in ascolto" } }, - "fr": { + "km": { "stringUnit": { "state": "translated", - "value": "%@ est la session que ce cmux enregistre actuellement. Exécutez cmux restore-session sans --from pour rouvrir le lancement précédent." + "value": "សេវាកម្មដែលកំពុងស្តាប់" } }, - "ar": { + "nb": { "stringUnit": { "state": "translated", - "value": "%@ هي الجلسة التي يحفظها cmux هذا الآن. شغّل cmux restore-session دون --from لإعادة فتح التشغيل السابق." + "value": "Lyttende tjenester" } }, - "es": { + "pl": { "stringUnit": { "state": "translated", - "value": "%@ es la sesión que este cmux está guardando ahora. Ejecuta cmux restore-session sin --from para volver a abrir el inicio anterior." + "value": "Nasłuchujące usługi" } }, - "zh-Hant": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "%@ 是此 cmux 目前正在儲存的工作階段。執行不含 --from 的 cmux restore-session 以重新開啟上次啟動的內容。" + "value": "Serviços em escuta" } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "%@ 是此 cmux 当前正在保存的会话。运行不带 --from 的 cmux restore-session 以重新打开上次启动的内容。" + "value": "Прослушивающие службы" } }, - "ko": { + "th": { "stringUnit": { "state": "translated", - "value": "%@ 파일은 이 cmux가 지금 저장 중인 세션입니다. 이전 실행을 다시 열려면 --from 없이 cmux restore-session을 실행하세요." + "value": "บริการที่กำลังรับฟัง" } }, - "ja": { + "tr": { "stringUnit": { "state": "translated", - "value": "%@ はこの cmux が現在保存しているセッションです。前回の起動を再度開くには、--from を付けずに cmux restore-session を実行してください。" + "value": "Dinleyen hizmetler" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Служби, що прослуховують" } } } }, - "machines.requiresPro.upgrade.short": { + "cloudTree.ports.binding.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Upgrade" + "value": "cmux’s browser route connects to 127.0.0.1 on the machine. Bind the service to 127.0.0.1 or 0.0.0.0, then refresh. Turning on Cloud VPN does not change this route." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "ترقية" + "value": "Die Browser-Verbindung von cmux verwendet 127.0.0.1 auf der Maschine. Binden Sie den Dienst an 127.0.0.1 oder 0.0.0.0 und aktualisieren Sie. Cloud-VPN ändert diese Route nicht." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Upgraden" + "value": "Le navigateur cmux se connecte à 127.0.0.1 sur la machine. Liez le service à 127.0.0.1 ou 0.0.0.0, puis actualisez. Activer le VPN Cloud ne change pas cette route." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يتصل مسار متصفح cmux بالعنوان 127.0.0.1 على الجهاز. اربط الخدمة بالعنوان 127.0.0.1 أو 0.0.0.0 ثم حدّث. تشغيل Cloud VPN لا يغير هذا المسار." } }, "es": { "stringUnit": { "state": "translated", - "value": "Actualizar" + "value": "La ruta del navegador de cmux conecta a 127.0.0.1 en la máquina. Enlaza el servicio a 127.0.0.1 o 0.0.0.0 y actualiza. Activar Cloud VPN no cambia esta ruta." } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Mettre à niveau" + "value": "cmux 的瀏覽器路徑會連線到機器上的 127.0.0.1。請將服務繫結至 127.0.0.1 或 0.0.0.0 後重新整理。開啟 Cloud VPN 不會改變此路徑。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "アップグレード" + "value": "cmux 的浏览器路由连接到机器上的 127.0.0.1。请将服务绑定到 127.0.0.1 或 0.0.0.0 后刷新。开启 Cloud VPN 不会改变此路由。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "업그레이드" + "value": "cmux 브라우저 경로는 머신의 127.0.0.1에 연결합니다. 서비스를 127.0.0.1 또는 0.0.0.0에 바인딩한 후 새로 고치세요. Cloud VPN을 켜도 이 경로는 바뀌지 않습니다." } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "升级" + "value": "cmux のブラウザはマシン上の 127.0.0.1 に接続します。サービスを 127.0.0.1 または 0.0.0.0 にバインドして更新してください。Cloud VPN を有効にしてもこの経路は変わりません。" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "升級" + "value": "Ruta preglednika u cmux-u povezuje se na 127.0.0.1 na mašini. Vežite servis na 127.0.0.1 ili 0.0.0.0, zatim osvježite. Uključivanje Cloud VPN-a ne mijenja ovu rutu." } - } - } - }, - "machines.sessionRejected.signInAgain.short": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Sign In" + "value": "cmux’ browserrute forbinder til 127.0.0.1 på maskinen. Bind tjenesten til 127.0.0.1 eller 0.0.0.0, og opdater derefter. Aktivering af Cloud VPN ændrer ikke denne rute." } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "تسجيل الدخول" + "value": "Il percorso del browser di cmux si connette a 127.0.0.1 sulla macchina. Associa il servizio a 127.0.0.1 o 0.0.0.0, quindi aggiorna. L’attivazione della VPN Cloud non modifica questo percorso." } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Anmelden" + "value": "ផ្លូវកម្មវិធីរុករករបស់ cmux ភ្ជាប់ទៅ 127.0.0.1 នៅលើម៉ាស៊ីន។ ចងសេវាកម្មទៅ 127.0.0.1 ឬ 0.0.0.0 រួចផ្ទុកឡើងវិញ។ ការបើក Cloud VPN មិនផ្លាស់ប្តូរផ្លូវនេះទេ។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Iniciar sesión" + "value": "cmux sin nettleserrute kobler til 127.0.0.1 på maskinen. Bind tjenesten til 127.0.0.1 eller 0.0.0.0, og oppdater deretter. Å slå på Cloud VPN endrer ikke denne ruten." } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Se reconnecter" + "value": "Trasa przeglądarki cmux łączy się z 127.0.0.1 na maszynie. Powiąż usługę z 127.0.0.1 lub 0.0.0.0, a następnie odśwież. Włączenie Cloud VPN nie zmienia tej trasy." } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "再サインイン" + "value": "A rota de navegador do cmux se conecta a 127.0.0.1 na máquina. Vincule o serviço a 127.0.0.1 ou 0.0.0.0 e atualize. Ativar o Cloud VPN não altera essa rota." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "다시 로그인" + "value": "Браузерный маршрут cmux подключается к 127.0.0.1 на машине. Привяжите службу к 127.0.0.1 или 0.0.0.0, затем обновите. Включение Cloud VPN не меняет этот маршрут." } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "重新登录" + "value": "เส้นทางเบราว์เซอร์ของ cmux เชื่อมต่อกับ 127.0.0.1 บนเครื่อง ผูกบริการกับ 127.0.0.1 หรือ 0.0.0.0 แล้วรีเฟรช การเปิด Cloud VPN ไม่ได้เปลี่ยนเส้นทางนี้" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "重新登入" + "value": "cmux’un tarayıcı yolu makinedeki 127.0.0.1 adresine bağlanır. Hizmeti 127.0.0.1 veya 0.0.0.0 adresine bağlayın, ardından yenileyin. Cloud VPN’i açmak bu yolu değiştirmez." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Браузерний маршрут cmux підключається до 127.0.0.1 на машині. Прив’яжіть службу до 127.0.0.1 або 0.0.0.0, потім оновіть. Увімкнення Cloud VPN не змінює цей маршрут." } } } }, - "machines.requiresPro.stale": { + "cloudTree.ports.binding.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud machines need cmux Pro, showing last known" + "value": "Services use another interface" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "تتطلب الأجهزة السحابية إصدار cmux Pro، يتم عرض آخر حالة معروفة" + "value": "Dienste verwenden eine andere Schnittstelle" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen erfordern cmux Pro, zeige letzten bekannten Stand" + "value": "Les services utilisent une autre interface" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Las máquinas en la nube requieren cmux Pro, se muestra el último estado conocido" + "value": "تستخدم الخدمات واجهة أخرى" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Les machines Cloud nécessitent cmux Pro, affichage des dernières informations connues" + "value": "Los servicios usan otra interfaz" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud マシンには cmux Pro が必要です。最後の状態を表示中" + "value": "服務使用其他介面" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "服务使用其他接口" } }, "ko": { "stringUnit": { "state": "translated", - "value": "클라우드 머신을 사용하려면 cmux Pro가 필요함, 마지막으로 확인된 상태 표시 중" + "value": "서비스가 다른 인터페이스를 사용함" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "云端机器需要 cmux Pro,显示的是最后已知状态" + "value": "サービスが別のインターフェイスを使用しています" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "雲端機器需要 cmux Pro,顯示上次已知的狀態" + "value": "Servisi koriste drugi interfejs" } - } - } - }, - "machines.sessionRejected.stale": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Sign-in needs a refresh, showing last known" + "value": "Tjenester bruger en anden grænseflade" } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "يحتاج تسجيل الدخول إلى تحديث، يتم عرض آخر حالة معروفة" + "value": "I servizi usano un’altra interfaccia" } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Anmeldung muss aktualisiert werden, zeige letzten bekannten Stand" + "value": "សេវាកម្មប្រើចំណុចប្រទាក់ផ្សេង" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Es necesario actualizar el inicio de sesión, se muestra el último estado conocido" + "value": "Tjenester bruker et annet grensesnitt" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "La connexion doit être actualisée, affichage des dernières informations connues" + "value": "Usługi używają innego interfejsu" } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "再サインインが必要です。最後の状態を表示中" + "value": "Os serviços usam outra interface" } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "로그인 갱신 필요, 마지막으로 확인된 상태 표시 중" + "value": "Службы используют другой интерфейс" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "登录状态需要刷新,显示的是最后已知状态" + "value": "บริการใช้อินเทอร์เฟซอื่น" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "登入狀態需要重新整理,顯示上次已知的狀態" + "value": "Hizmetler başka bir arabirim kullanıyor" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Служби використовують інший інтерфейс" } } } }, - "cli.session.help": { + "cloudTree.ports.empty.noService": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Usage: cmux session restore [--list] [--session ]...\n\nReopen Claude sessions that were running when cmux last quit unexpectedly.\ncmux finds them in the agent journal, skips any that are running or already\nopen, and resumes each in its own workspace through the launcher that started\nit.\n\nWithout --session, restore acts only after an unexpected quit.\n\nOptions:\n --list Show the sessions without restoring them.\n --session Restore only this session (repeatable)." + "value": "Start a web server on this machine, then refresh." } }, "de": { "stringUnit": { "state": "translated", - "value": "Verwendung: cmux session restore [--list] [--session ]...\n\nÖffnet Claude-Sitzungen erneut, die liefen, als cmux zuletzt unerwartet beendet wurde.\ncmux findet sie im Agent-Journal, überspringt laufende oder bereits geöffnete\nund setzt jede in einem eigenen Workspace\nüber den Launcher fort, der sie gestartet hat.\n\nOhne --session stellt restore nur nach einem unerwarteten Beenden wieder her.\n\nOptionen:\n --list Sitzungen anzeigen, ohne sie wiederherzustellen.\n --session Nur diese Sitzung wiederherstellen (wiederholbar)." + "value": "Starte einen Webserver auf diesem Rechner und aktualisiere dann die Liste." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utilisation : cmux session restore [--list] [--session ]...\n\nRouvre les sessions Claude en cours lorsque cmux s’est fermé de façon inattendue.\ncmux les trouve dans le journal des agents, ignore celles qui tournent ou sont déjà\nouvertes, et reprend chacune dans son propre espace de travail via le lanceur qui l’a\ndémarrée.\n\nSans --session, restore n’agit qu’après une fermeture inattendue.\n\nOptions :\n --list Afficher les sessions sans les restaurer.\n --session Restaurer uniquement cette session (répétable)." + "value": "Démarrez un serveur web sur cette machine, puis actualisez." } }, "ar": { "stringUnit": { "state": "translated", - "value": "الاستخدام: cmux session restore [--list] [--session ]...\n\nيعيد فتح جلسات Claude التي كانت قيد التشغيل عندما أُغلق cmux بشكل غير متوقع آخر مرة.\nيعثر cmux عليها في سجل الوكيل، ويتخطى ما يعمل منها أو المفتوح بالفعل،\nويستأنف كل جلسة في مساحة عمل خاصة بها\nعبر المشغّل الذي بدأها.\n\nبدون --session، لا تعمل الاستعادة إلا بعد إغلاق غير متوقع.\n\nالخيارات:\n --list عرض الجلسات دون استعادتها.\n --session استعادة هذه الجلسة فقط (قابل للتكرار)." + "value": "شغّل خادم ويب على هذا الجهاز، ثم حدّث القائمة." } }, "es": { "stringUnit": { "state": "translated", - "value": "Uso: cmux session restore [--list] [--session ]...\n\nVuelve a abrir las sesiones de Claude que estaban en ejecución cuando cmux se cerró inesperadamente.\ncmux las encuentra en el diario de agentes, omite las que están en ejecución o ya\nabiertas, y reanuda cada una en su propio espacio de trabajo mediante el lanzador que\nla inició.\n\nSin --session, restore solo actúa tras un cierre inesperado.\n\nOpciones:\n --list Mostrar las sesiones sin restaurarlas.\n --session Restaurar solo esta sesión (repetible)." + "value": "Inicia un servidor web en esta máquina y luego actualiza." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "用法: cmux session restore [--list] [--session ]...\n\n重新開啟 cmux 上次意外結束時仍在執行的 Claude 工作階段。\ncmux 會從代理日誌中找出它們,略過仍在執行或已開啟的工作階段,\n並透過原本啟動它的啟動器\n在各自的工作區中繼續。\n\n未指定 --session 時,restore 只會在意外結束後執行。\n\n選項:\n --list 只列出工作階段,不還原。\n --session 只還原此工作階段(可重複)。" + "value": "在這台機器上啟動網頁伺服器,然後重新整理。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用法: cmux session restore [--list] [--session ]...\n\n重新打开 cmux 上次意外退出时仍在运行的 Claude 会话。\ncmux 会从代理日志中找到它们,跳过仍在运行或已打开的会话,\n并通过最初启动它的启动器\n在各自的工作区中继续。\n\n未指定 --session 时,restore 只会在意外退出后执行。\n\n选项:\n --list 只列出会话,不恢复。\n --session 只恢复此会话(可重复)。" + "value": "在此机器上启动网页服务器,然后刷新。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "사용법: cmux session restore [--list] [--session ]...\n\ncmux가 마지막으로 예기치 않게 종료될 때 실행 중이던 Claude 세션을 다시 엽니다.\ncmux는 에이전트 저널에서 세션을 찾고, 실행 중이거나 이미 열려 있는 세션은 건너뛰며,\n각 세션을 시작한 런처를 통해\n자체 워크스페이스에서 이어갑니다.\n\n--session이 없으면 restore는 예기치 않은 종료 후에만 동작합니다.\n\n옵션:\n --list 복원하지 않고 세션만 표시합니다.\n --session 이 세션만 복원합니다(반복 가능)." + "value": "이 머신에서 웹 서버를 시작한 다음 새로 고치세요." } }, "ja": { "stringUnit": { "state": "translated", - "value": "使用法: cmux session restore [--list] [--session ]...\n\ncmux が前回予期せず終了したときに実行中だった Claude セッションを再び開きます。\ncmux はエージェントジャーナルからセッションを見つけ、実行中または既に開いている\nものはスキップし、それぞれを起動したランチャー経由で\n専用のワークスペースで再開します。\n\n--session を指定しない場合、restore は予期しない終了の後にのみ動作します。\n\nオプション:\n --list 復元せずにセッションを表示します。\n --session このセッションのみ復元します(複数指定可)。" + "value": "このマシンで Web サーバーを起動してから、更新してください。" + } + }, + "bs": { + "stringUnit": { + "state": "needs_review", + "value": "Pokrenite web server na ovoj mašini, zatim osvježite." + } + }, + "da": { + "stringUnit": { + "state": "needs_review", + "value": "Start en webserver på denne maskine, og opdater derefter." + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "Avvia un server web su questa macchina, poi aggiorna." + } + }, + "km": { + "stringUnit": { + "state": "needs_review", + "value": "ចាប់ផ្តើមម៉ាស៊ីនមេបណ្តាញនៅលើម៉ាស៊ីននេះ រួចផ្ទុកឡើងវិញ។" + } + }, + "nb": { + "stringUnit": { + "state": "needs_review", + "value": "Start en webserver på denne maskinen, og oppdater deretter." + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "Uruchom serwer WWW na tej maszynie, a następnie odśwież." + } + }, + "pt-BR": { + "stringUnit": { + "state": "needs_review", + "value": "Inicie um servidor web nesta máquina e atualize." + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "Запустите веб-сервер на этой машине, затем обновите список." + } + }, + "th": { + "stringUnit": { + "state": "needs_review", + "value": "เริ่มเว็บเซิร์ฟเวอร์บนเครื่องนี้ แล้วรีเฟรช" + } + }, + "tr": { + "stringUnit": { + "state": "needs_review", + "value": "Bu makinede bir web sunucusu başlatın, ardından yenileyin." + } + }, + "uk": { + "stringUnit": { + "state": "needs_review", + "value": "Запустіть вебсервер на цій машині, а потім оновіть список." } } } }, - "cli.session.error.expectedRestore": { + "cloudTree.ports.hub.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "session: expected 'restore' (see cmux help session)" + "value": "This build cannot start cmux’s authenticated Cloud forward. Update cmux, then retry." } }, "de": { "stringUnit": { "state": "translated", - "value": "session: 'restore' erwartet (siehe cmux help session)" + "value": "Dieser Build kann die authentifizierte Cloud-Weiterleitung von cmux nicht starten. Aktualisieren Sie cmux und versuchen Sie es erneut." } }, "fr": { "stringUnit": { "state": "translated", - "value": "session : « restore » attendu (voir cmux help session)" + "value": "Cette version ne peut pas démarrer le transfert Cloud authentifié de cmux. Mettez cmux à jour, puis réessayez." } }, "ar": { "stringUnit": { "state": "translated", - "value": "session: كان المتوقع 'restore' (راجع cmux help session)" + "value": "لا يمكن لهذا الإصدار بدء إعادة توجيه Cloud الموثّقة من cmux. حدّث cmux ثم أعد المحاولة." } }, "es": { "stringUnit": { "state": "translated", - "value": "session: se esperaba 'restore' (consulta cmux help session)" + "value": "Esta compilación no puede iniciar el reenvío autenticado de Cloud de cmux. Actualiza cmux y vuelve a intentarlo." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "session:應為 'restore'(請參閱 cmux help session)" + "value": "此版本無法啟動 cmux 的已驗證 Cloud 轉送。請更新 cmux 後重試。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "session:应为 'restore'(请参阅 cmux help session)" + "value": "此版本无法启动 cmux 的已验证 Cloud 转发。请更新 cmux 后重试。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "session: 'restore'가 필요합니다(cmux help session 참조)" + "value": "이 빌드에서는 cmux 인증 Cloud 전달을 시작할 수 없습니다. cmux를 업데이트한 후 다시 시도하세요." } }, "ja": { "stringUnit": { "state": "translated", - "value": "session: 'restore' が必要です(cmux help session を参照)" + "value": "このビルドでは cmux の認証済み Cloud 転送を開始できません。cmux を更新して再試行してください。" } - } - } - }, - "cli.session.error.sessionRequiresId": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "session restore: --session requires an id" + "value": "Ova verzija ne može pokrenuti autentificirano Cloud prosljeđivanje cmux-a. Ažurirajte cmux, zatim pokušajte ponovo." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "session restore: --session erfordert eine ID" + "value": "Denne build kan ikke starte cmux’ godkendte Cloud-videresendelse. Opdater cmux, og prøv igen." } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "session restore : --session nécessite un identifiant" + "value": "Questa build non può avviare l’inoltro Cloud autenticato di cmux. Aggiorna cmux, quindi riprova." } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "session restore: يتطلب --session معرّفًا" + "value": "កំណែនេះមិនអាចចាប់ផ្តើមការបញ្ជូនបន្ត Cloud ដែលបានផ្ទៀងផ្ទាត់របស់ cmux បានទេ។ ធ្វើបច្ចុប្បន្នភាព cmux រួចព្យាយាមម្តងទៀត។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "session restore: --session requiere un id" + "value": "Denne versjonen kan ikke starte cmux sin autentiserte Cloud-videresending. Oppdater cmux, og prøv igjen." } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "session restore:--session 需要一個 ID" + "value": "Ta kompilacja nie może uruchomić uwierzytelnionego przekierowania Cloud w cmux. Zaktualizuj cmux i spróbuj ponownie." } }, - "zh-Hans": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "session restore:--session 需要一个 ID" + "value": "Esta versão não consegue iniciar o encaminhamento Cloud autenticado do cmux. Atualize o cmux e tente novamente." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "session restore: --session에는 ID가 필요합니다" + "value": "Эта сборка не может запустить аутентифицированное перенаправление Cloud в cmux. Обновите cmux и повторите попытку." } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "session restore: --session には ID が必要です" + "value": "บิลด์นี้ไม่สามารถเริ่มการส่งต่อ Cloud ที่ยืนยันตัวตนแล้วของ cmux ได้ อัปเดต cmux แล้วลองอีกครั้ง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu derleme cmux’un kimliği doğrulanmış Cloud yönlendirmesini başlatamıyor. cmux’u güncelleyin, ardından yeniden deneyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ця збірка не може запустити автентифіковане перенаправлення Cloud у cmux. Оновіть cmux і повторіть спробу." } } } }, - "cli.session.error.unknownArgument": { + "cloudTree.ports.link.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "session restore: unknown argument '%@'" + "value": "Port discovery could not connect to this machine. Existing cmux ports may still work. Refresh to reconnect; Cloud VPN is not required." } }, "de": { "stringUnit": { "state": "translated", - "value": "session restore: unbekanntes Argument '%@'" + "value": "Die Porterkennung konnte diese Maschine nicht erreichen. Bereits geöffnete cmux-Ports funktionieren möglicherweise weiter. Aktualisieren Sie zum erneuten Verbinden; Cloud-VPN ist nicht erforderlich." } }, "fr": { "stringUnit": { "state": "translated", - "value": "session restore : argument inconnu « %@ »" + "value": "La découverte des ports n’a pas pu se connecter à cette machine. Les ports cmux existants peuvent encore fonctionner. Actualisez pour reconnecter ; le VPN Cloud n’est pas nécessaire." } }, "ar": { "stringUnit": { "state": "translated", - "value": "session restore: وسيط غير معروف '%@'" + "value": "تعذر اتصال اكتشاف المنافذ بهذا الجهاز. قد تظل منافذ cmux الحالية تعمل. حدّث لإعادة الاتصال؛ Cloud VPN غير مطلوب." } }, "es": { "stringUnit": { "state": "translated", - "value": "session restore: argumento desconocido '%@'" + "value": "El descubrimiento de puertos no pudo conectar con esta máquina. Los puertos de cmux existentes pueden seguir funcionando. Actualiza para reconectar; Cloud VPN no es necesario." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "session restore:未知的參數 '%@'" + "value": "連接埠探索無法連線到此機器。現有 cmux 連接埠可能仍可使用。請重新整理以重新連線;不需要 Cloud VPN。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "session restore:未知参数 '%@'" + "value": "端口发现无法连接到此机器。现有 cmux 端口可能仍可使用。请刷新以重新连接;无需 Cloud VPN。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "session restore: 알 수 없는 인수 '%@'" + "value": "포트 검색이 이 머신에 연결하지 못했습니다. 기존 cmux 포트는 계속 작동할 수 있습니다. 새로 고쳐 다시 연결하세요. Cloud VPN은 필요하지 않습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "session restore: 不明な引数 '%@'" + "value": "ポート検出がこのマシンに接続できませんでした。既存の cmux ポートは引き続き動作する可能性があります。更新して再接続してください。Cloud VPN は不要です。" } - } - } - }, - "cli.session.list.empty": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "No agent sessions to restore." + "value": "Otkrivanje portova nije se moglo povezati s ovom mašinom. Postojeći cmux portovi možda i dalje rade. Osvježite za ponovno povezivanje; Cloud VPN nije potreban." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "Keine Agent-Sitzungen zum Wiederherstellen." + "value": "Portregistrering kunne ikke oprette forbindelse til denne maskine. Eksisterende cmux-porte virker muligvis stadig. Opdater for at genoprette forbindelsen; Cloud VPN er ikke påkrævet." } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "Aucune session d’agent à restaurer." + "value": "Il rilevamento delle porte non è riuscito a connettersi a questa macchina. Le porte cmux esistenti potrebbero funzionare ancora. Aggiorna per riconnetterti; la VPN Cloud non è necessaria." } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "لا توجد جلسات وكيل لاستعادتها." + "value": "ការស្វែងរកច្រកមិនអាចភ្ជាប់ទៅម៉ាស៊ីននេះបានទេ។ ច្រក cmux ដែលមានស្រាប់អាចនៅតែដំណើរការ។ ផ្ទុកឡើងវិញដើម្បីភ្ជាប់ម្តងទៀត; មិនត្រូវការ Cloud VPN ទេ។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "No hay sesiones de agente para restaurar." + "value": "Portoppdagelse kunne ikke koble til denne maskinen. Eksisterende cmux-porter kan fortsatt fungere. Oppdater for å koble til på nytt; Cloud VPN er ikke nødvendig." } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "沒有可還原的代理工作階段。" + "value": "Wykrywanie portów nie mogło połączyć się z tą maszyną. Istniejące porty cmux mogą nadal działać. Odśwież, aby połączyć ponownie; Cloud VPN nie jest wymagany." } }, - "zh-Hans": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "没有可恢复的代理会话。" + "value": "A descoberta de portas não conseguiu se conectar a esta máquina. As portas existentes do cmux ainda podem funcionar. Atualize para reconectar; o Cloud VPN não é necessário." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "복원할 에이전트 세션이 없습니다." + "value": "Обнаружению портов не удалось подключиться к этой машине. Существующие порты cmux могут по-прежнему работать. Обновите, чтобы переподключиться; Cloud VPN не требуется." } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "復元するエージェントセッションはありません。" + "value": "การค้นหาพอร์ตไม่สามารถเชื่อมต่อกับเครื่องนี้ได้ พอร์ต cmux ที่มีอยู่อาจยังใช้งานได้ รีเฟรชเพื่อเชื่อมต่อใหม่ ไม่จำเป็นต้องใช้ Cloud VPN" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Port keşfi bu makineye bağlanamadı. Mevcut cmux portları hâlâ çalışıyor olabilir. Yeniden bağlanmak için yenileyin; Cloud VPN gerekmez." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Виявленню портів не вдалося підключитися до цієї машини. Наявні порти cmux можуть і далі працювати. Оновіть, щоб підключитися знову; Cloud VPN не потрібен." } } } }, - "cli.session.restore.cleanQuit": { + "cloudTree.ports.link.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No agent sessions restored: cmux last quit normally. To restore one anyway, pass --session from --list." + "value": "Cloud link unavailable" } }, "de": { "stringUnit": { "state": "translated", - "value": "Keine Agent-Sitzungen wiederhergestellt: cmux wurde zuletzt normal beendet. Um trotzdem eine wiederherzustellen, übergib --session aus --list." + "value": "Cloud-Verbindung nicht verfügbar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Aucune session d’agent restaurée : cmux s’est fermé normalement la dernière fois. Pour en restaurer une quand même, passez --session depuis --list." + "value": "Connexion Cloud indisponible" } }, "ar": { "stringUnit": { "state": "translated", - "value": "لم تتم استعادة أي جلسات وكيل: أُغلق cmux بشكل طبيعي آخر مرة. لاستعادة جلسة على أي حال، مرّر --session من --list." + "value": "اتصال Cloud غير متاح" } }, "es": { "stringUnit": { "state": "translated", - "value": "No se restauró ninguna sesión de agente: cmux se cerró normalmente la última vez. Para restaurar una de todos modos, pasa --session de --list." + "value": "Conexión de Cloud no disponible" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "未還原任何代理工作階段:cmux 上次是正常結束。若仍要還原,請傳入 --list 中的 --session 。" + "value": "Cloud 連線無法使用" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "未恢复任何代理会话:cmux 上次是正常退出。若仍要恢复,请传入 --list 中的 --session 。" + "value": "Cloud 连接不可用" } }, "ko": { "stringUnit": { "state": "translated", - "value": "복원된 에이전트 세션이 없습니다. cmux가 마지막에 정상적으로 종료되었습니다. 그래도 복원하려면 --list에서 확인한 --session 를 전달하세요." + "value": "Cloud 연결을 사용할 수 없음" } }, "ja": { "stringUnit": { "state": "translated", - "value": "エージェントセッションは復元されませんでした。cmux は前回正常に終了しています。それでも復元する場合は、--list の --session を指定してください。" + "value": "Cloud 接続を利用できません" } - } - } - }, - "cli.session.restore.empty": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "No agent sessions restored." + "value": "Cloud veza nije dostupna" } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "Keine Agent-Sitzungen wiederhergestellt." + "value": "Cloud-forbindelse utilgængelig" } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "Aucune session d’agent restaurée." + "value": "Collegamento Cloud non disponibile" } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "لم تتم استعادة أي جلسات وكيل." + "value": "តំណ Cloud មិនអាចប្រើបាន" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "No se restauró ninguna sesión de agente." + "value": "Cloud-tilkobling utilgjengelig" } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "未還原任何代理工作階段。" + "value": "Połączenie Cloud niedostępne" } }, - "zh-Hans": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "未恢复任何代理会话。" + "value": "Conexão Cloud indisponível" } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "복원된 에이전트 세션이 없습니다." + "value": "Связь с Cloud недоступна" } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "エージェントセッションは復元されませんでした。" + "value": "ลิงก์ Cloud ไม่พร้อมใช้งาน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Cloud bağlantısı kullanılamıyor" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Зв’язок із Cloud недоступний" } } } }, - "cli.session.restore.done": { + "cloudTree.ports.loading.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Agent sessions restored: %lld" + "value": "Checking services through cmux’s authenticated Cloud link. No system VPN is needed." } }, "de": { "stringUnit": { "state": "translated", - "value": "Wiederhergestellte Agent-Sitzungen: %lld" + "value": "Dienste werden über die authentifizierte Cloud-Verbindung von cmux geprüft. Ein System-VPN ist nicht erforderlich." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Sessions d’agent restaurées : %lld" + "value": "Vérification des services via la connexion Cloud authentifiée de cmux. Aucun VPN système n’est nécessaire." } }, "ar": { "stringUnit": { "state": "translated", - "value": "جلسات الوكيل المستعادة: %lld" + "value": "جارٍ فحص الخدمات عبر اتصال Cloud الموثّق من cmux. لا حاجة إلى VPN النظام." } }, "es": { "stringUnit": { "state": "translated", - "value": "Sesiones de agente restauradas: %lld" + "value": "Comprobando servicios mediante la conexión autenticada de Cloud de cmux. No se necesita una VPN del sistema." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已還原的代理工作階段:%lld" + "value": "正在透過 cmux 的已驗證 Cloud 連線檢查服務。不需要系統 VPN。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已恢复的代理会话:%lld" + "value": "正在通过 cmux 的已验证 Cloud 连接检查服务。无需系统 VPN。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "복원된 에이전트 세션: %lld" + "value": "cmux의 인증된 Cloud 연결로 서비스를 확인하고 있습니다. 시스템 VPN은 필요하지 않습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "復元したエージェントセッション: %lld" + "value": "cmux の認証済み Cloud 接続でサービスを確認しています。システム VPN は不要です。" } - } - } - }, - "agentRecovery.alert.title": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "Restore agent sessions?" + "value": "Provjera servisa putem autentificirane Cloud veze cmux-a. Sistemski VPN nije potreban." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "Agent-Sitzungen wiederherstellen?" + "value": "Kontrollerer tjenester via cmux’ godkendte Cloud-forbindelse. Der kræves ingen system-VPN." } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "Restaurer les sessions d’agent ?" + "value": "Controllo dei servizi tramite il collegamento Cloud autenticato di cmux. Non serve una VPN di sistema." } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "هل تريد استعادة جلسات الوكيل؟" + "value": "កំពុងពិនិត្យសេវាកម្មតាមរយៈតំណ Cloud ដែលបានផ្ទៀងផ្ទាត់របស់ cmux។ មិនត្រូវការ VPN របស់ប្រព័ន្ធទេ។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "¿Restaurar las sesiones de agente?" + "value": "Sjekker tjenester via cmux sin autentiserte Cloud-tilkobling. Ingen system-VPN er nødvendig." } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "要還原代理工作階段嗎?" + "value": "Sprawdzanie usług przez uwierzytelnione połączenie Cloud w cmux. Systemowa sieć VPN nie jest potrzebna." } }, - "zh-Hans": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "要恢复代理会话吗?" + "value": "Verificando serviços pela conexão Cloud autenticada do cmux. Nenhuma VPN do sistema é necessária." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "에이전트 세션을 복원할까요?" + "value": "Проверка служб через аутентифицированную связь cmux с Cloud. Системный VPN не нужен." } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "エージェントセッションを復元しますか?" + "value": "กำลังตรวจสอบบริการผ่านลิงก์ Cloud ที่ยืนยันตัวตนแล้วของ cmux ไม่ต้องใช้ VPN ของระบบ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Hizmetler cmux’un kimliği doğrulanmış Cloud bağlantısı üzerinden denetleniyor. Sistem VPN’i gerekmez." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Перевірка служб через автентифікований зв’язок cmux із Cloud. Системний VPN не потрібен." } } } }, - "agentRecovery.alert.message": { + "cloudTree.ports.loopback.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux quit unexpectedly while agent sessions were running. Sessions to restore: %lld. Each one reopens in its own workspace and resumes where it left off." + "value": "These services open in cmux without Cloud VPN. They listen on the machine’s loopback address, so other apps cannot reach them through its private IP, even with VPN connected." } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux wurde unerwartet beendet, während Agent-Sitzungen liefen. Wiederherzustellende Sitzungen: %lld. Jede wird in einem eigenen Workspace geöffnet und dort fortgesetzt, wo sie aufgehört hat." + "value": "Diese Dienste lassen sich in cmux ohne Cloud-VPN öffnen. Sie lauschen auf der Loopback-Adresse der Maschine. Andere Apps erreichen sie daher auch mit VPN nicht über deren private IP." } }, "fr": { "stringUnit": { "state": "translated", - "value": "cmux s’est fermé de façon inattendue pendant que des sessions d’agent étaient en cours. Sessions à restaurer : %lld. Chacune rouvre dans son propre espace de travail et reprend là où elle s’était arrêtée." + "value": "Ces services s’ouvrent dans cmux sans VPN Cloud. Ils écoutent sur l’adresse locale de la machine : les autres apps ne peuvent pas y accéder via son IP privée, même avec le VPN." } }, "ar": { "stringUnit": { "state": "translated", - "value": "أُغلق cmux بشكل غير متوقع أثناء تشغيل جلسات الوكيل. الجلسات المطلوب استعادتها: %lld. تُفتح كل جلسة في مساحة عمل خاصة بها وتُستأنف من حيث توقفت." + "value": "تُفتح هذه الخدمات داخل cmux دون Cloud VPN. وهي تستمع على عنوان الواجهة المحلية للجهاز، لذا لا تستطيع التطبيقات الأخرى الوصول إليها عبر عنوان IP الخاص به حتى مع اتصال VPN." } }, "es": { "stringUnit": { "state": "translated", - "value": "cmux se cerró inesperadamente mientras había sesiones de agente en ejecución. Sesiones por restaurar: %lld. Cada una se abre en su propio espacio de trabajo y continúa donde se quedó." + "value": "Estos servicios se abren en cmux sin Cloud VPN. Escuchan en la dirección de bucle local de la máquina, por lo que otras apps no pueden acceder por su IP privada, incluso con la VPN conectada." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux 在代理工作階段執行時意外結束。要還原的工作階段:%lld。每個工作階段都會在各自的工作區重新開啟,並從中斷處繼續。" + "value": "這些服務可在 cmux 中開啟,不需要 Cloud VPN。它們在機器的迴路位址上監聽,因此即使已連線 VPN,其他 App 也無法透過其私人 IP 存取。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 在代理会话运行时意外退出。要恢复的会话:%lld。每个会话都会在各自的工作区重新打开,并从中断处继续。" + "value": "这些服务可在 cmux 中打开,无需 Cloud VPN。它们监听机器的回环地址,因此即使已连接 VPN,其他应用也无法通过其专用 IP 访问。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "에이전트 세션이 실행 중일 때 cmux가 예기치 않게 종료되었습니다. 복원할 세션: %lld. 각 세션은 자체 워크스페이스에서 다시 열리고 중단된 곳에서 이어집니다." + "value": "이 서비스는 Cloud VPN 없이 cmux에서 열립니다. 머신의 루프백 주소에서 수신 대기하므로 VPN에 연결해도 다른 앱은 비공개 IP로 접근할 수 없습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "エージェントセッションの実行中に cmux が予期せず終了しました。復元するセッション: %lld。各セッションは専用のワークスペースで再び開き、中断したところから再開します。" + "value": "これらのサービスは Cloud VPN なしで cmux から開けます。マシンのループバックアドレスで待ち受けるため、VPN 接続中でも他のアプリからプライベート IP 経由ではアクセスできません。" } - } - } - }, - "agentRecovery.alert.restore": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "Restore Agent Sessions" + "value": "Ovi servisi se otvaraju u cmux-u bez Cloud VPN-a. Slušaju na loopback adresi mašine, pa ih druge aplikacije ne mogu dosegnuti preko njene privatne IP adrese, čak ni uz povezan VPN." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "Agent-Sitzungen wiederherstellen" + "value": "Disse tjenester åbnes i cmux uden Cloud VPN. De lytter på maskinens loopback-adresse, så andre apps kan ikke nå dem via dens private IP, heller ikke med VPN forbundet." } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "Restaurer les sessions d’agent" + "value": "Questi servizi si aprono in cmux senza VPN Cloud. Sono in ascolto sull’indirizzo di loopback della macchina, quindi le altre app non possono raggiungerli tramite il suo IP privato, nemmeno con la VPN connessa." } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "استعادة جلسات الوكيل" + "value": "សេវាកម្មទាំងនេះបើកនៅក្នុង cmux ដោយមិនត្រូវការ Cloud VPN។ ពួកវាស្តាប់នៅលើអាសយដ្ឋាន loopback របស់ម៉ាស៊ីន ដូច្នេះកម្មវិធីផ្សេងទៀតមិនអាចចូលដល់ពួកវាតាមរយៈ IP ឯកជនរបស់វាបានទេ ទោះបីភ្ជាប់ VPN ក៏ដោយ។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Restaurar sesiones de agente" + "value": "Disse tjenestene åpnes i cmux uten Cloud VPN. De lytter på maskinens loopback-adresse, så andre apper kan ikke nå dem via den private IP-adressen, selv med VPN tilkoblet." } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "還原代理工作階段" + "value": "Te usługi otwierają się w cmux bez Cloud VPN. Nasłuchują na adresie pętli zwrotnej maszyny, więc inne aplikacje nie mogą do nich dotrzeć przez jej prywatny adres IP, nawet przy połączonym VPN." } }, - "zh-Hans": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "恢复代理会话" + "value": "Esses serviços abrem no cmux sem o Cloud VPN. Eles escutam no endereço de loopback da máquina, então outros apps não conseguem alcançá-los pelo IP privado dela, mesmo com a VPN conectada." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "에이전트 세션 복원" + "value": "Эти службы открываются в cmux без Cloud VPN. Они прослушивают loopback-адрес машины, поэтому другие приложения не могут обратиться к ним через её частный IP даже при подключённом VPN." } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "エージェントセッションを復元" + "value": "บริการเหล่านี้เปิดใน cmux ได้โดยไม่ต้องใช้ Cloud VPN บริการรับฟังบนที่อยู่ loopback ของเครื่อง แอปอื่นจึงเข้าถึงผ่าน IP ส่วนตัวของเครื่องไม่ได้ แม้จะเชื่อมต่อ VPN แล้วก็ตาม" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu hizmetler cmux’ta Cloud VPN olmadan açılır. Makinenin geri döngü adresinde dinledikleri için diğer uygulamalar, VPN bağlıyken bile makinenin özel IP’si üzerinden onlara ulaşamaz." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ці служби відкриваються в cmux без Cloud VPN. Вони прослуховують loopback-адресу машини, тому інші застосунки не можуть дістатися до них через її приватну IP-адресу, навіть із підключеним VPN." } } } }, - "agentRecovery.alert.notNow": { + "cloudTree.ports.loopback.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Not Now" + "value": "Loopback services available" } }, "de": { "stringUnit": { "state": "translated", - "value": "Nicht jetzt" + "value": "Loopback-Dienste verfügbar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Pas maintenant" + "value": "Services locaux disponibles" } }, "ar": { "stringUnit": { "state": "translated", - "value": "ليس الآن" + "value": "خدمات الواجهة المحلية متاحة" } }, "es": { "stringUnit": { "state": "translated", - "value": "Ahora no" + "value": "Servicios de bucle local disponibles" } }, "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": "ループバックのサービスを利用できます" } - } - } - }, - "settings.notifications.soundWhenFocused.subtitle": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "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." + "value": "Dostupni loopback servisi" } }, - "ar": { + "da": { "stringUnit": { "state": "translated", - "value": "شغّل صوت الإشعار حتى عندما تكون اللوحة التي أرسلت الإشعار في موضع التركيز بالفعل. عند إيقافه، تعرض اللوحة المُركَّز عليها حلقتها دون صوت." + "value": "Loopback-tjenester tilgængelige" } }, - "de": { + "it": { "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." + "value": "Servizi di loopback disponibili" } }, - "es": { + "km": { "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." + "value": "មានសេវាកម្ម loopback" } }, - "fr": { + "nb": { "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." + "value": "Loopback-tjenester tilgjengelige" } }, - "ja": { + "pl": { "stringUnit": { "state": "translated", - "value": "通知を送ったペインがすでにフォーカスされている場合でも通知音を鳴らします。オフの場合、フォーカス中のペインは音を鳴らさずにリングを表示します。" + "value": "Dostępne usługi pętli zwrotnej" } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "알림을 보낸 패널이 이미 포커스된 상태여도 알림음을 재생합니다. 끄면 포커스된 패널은 소리 없이 링만 표시합니다." + "value": "Serviços de loopback disponíveis" } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "即使发出通知的窗格已处于焦点状态,也播放通知声音。关闭时,处于焦点的窗格会显示光环但不发出声音。" + "value": "Доступны loopback-службы" } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "即使發出通知的窗格已在焦點中,也播放通知聲音。關閉時,焦點中的窗格會顯示光環但不發出聲音。" + "value": "มีบริการ loopback พร้อมใช้งาน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Geri döngü hizmetleri kullanılabilir" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Доступні loopback-служби" } } } }, - "settings.notifications.soundWhenFocused.title": { + "cloudTree.ports.notRequested.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Sound for Focused Pane" + "value": "Ports not checked yet" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "صوت للوحة المُركَّز عليها" + "value": "Ports noch nicht geprüft" } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Ton für fokussierten Bereich" + "value": "Ports pas encore vérifiés" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يتم فحص المنافذ بعد" } }, "es": { "stringUnit": { "state": "translated", - "value": "Sonido para el panel con foco" + "value": "Puertos aún sin comprobar" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Son pour le panneau actif" + "value": "尚未檢查連接埠" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "フォーカス中のペインの通知音" + "value": "尚未检查端口" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포커스된 패널 소리" + "value": "아직 포트를 확인하지 않음" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "焦点窗格的声音" + "value": "ポートは未確認です" } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "焦點窗格的聲音" + "value": "Portovi još nisu provjereni" } - } - } - }, - "agent.claude.stopFailure.body.generic": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Claude Code stopped on an API error" + "value": "Porte er ikke kontrolleret endnu" } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "Claude Code は API エラーで停止しました" + "value": "Porte non ancora controllate" } }, - "zh-Hans": { + "km": { "stringUnit": { "state": "translated", - "value": "Claude Code 因 API 错误而停止" + "value": "មិនទាន់បានពិនិត្យច្រកនៅឡើយ" } }, - "de": { + "nb": { "stringUnit": { "state": "translated", - "value": "Claude Code wurde wegen eines API-Fehlers gestoppt" + "value": "Porter er ikke sjekket ennå" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Claude Code s'est arrêté sur une erreur d'API" + "value": "Porty nie zostały jeszcze sprawdzone" } }, - "ar": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "توقف Claude Code بسبب خطأ في واجهة API" + "value": "Portas ainda não verificadas" } }, - "es": { + "ru": { "stringUnit": { "state": "translated", - "value": "Claude Code se detuvo por un error de la API" + "value": "Порты ещё не проверены" } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "Claude Code 因 API 錯誤而停止" + "value": "ยังไม่ได้ตรวจสอบพอร์ต" } }, - "ko": { + "tr": { "stringUnit": { "state": "translated", - "value": "Claude Code가 API 오류로 중지되었습니다" + "value": "Portlar henüz denetlenmedi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Порти ще не перевірено" } } } }, - "agent.claude.stopFailure.status.apiError": { + "cloudTree.ports.privateAddress": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "API error" + "value": "Private address unavailable" } }, - "ja": { + "de": { "stringUnit": { "state": "translated", - "value": "API エラー" + "value": "Private Adresse nicht verfügbar" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "API 错误" + "value": "Adresse privée indisponible" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "API-Fehler" + "value": "العنوان الخاص غير متاح" } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Erreur d'API" + "value": "Dirección privada no disponible" } }, - "ar": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "خطأ في واجهة API" + "value": "私人位址無法使用" } }, - "es": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Error de la API" + "value": "专用地址不可用" } }, - "zh-Hant": { + "ko": { "stringUnit": { "state": "translated", - "value": "API 錯誤" + "value": "비공개 주소를 사용할 수 없음" } }, - "ko": { + "ja": { "stringUnit": { "state": "translated", - "value": "API 오류" + "value": "プライベートアドレスを利用できません" } - } - } - }, - "agent.claude.stopFailure.status.authentication": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "Auth error" + "value": "Privatna adresa nije dostupna" } }, - "ja": { + "da": { "stringUnit": { "state": "translated", - "value": "認証エラー" + "value": "Privat adresse utilgængelig" } }, - "zh-Hans": { + "it": { "stringUnit": { "state": "translated", - "value": "认证错误" + "value": "Indirizzo privato non disponibile" } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Authentifizierungsfehler" + "value": "អាសយដ្ឋានឯកជនមិនអាចប្រើបាន" } }, - "fr": { + "nb": { "stringUnit": { "state": "translated", - "value": "Erreur d'authentification" + "value": "Privat adresse utilgjengelig" } }, - "ar": { + "pl": { "stringUnit": { "state": "translated", - "value": "خطأ في المصادقة" + "value": "Adres prywatny niedostępny" } }, - "es": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Error de autenticación" + "value": "Endereço privado indisponível" } }, - "zh-Hant": { + "ru": { "stringUnit": { "state": "translated", - "value": "驗證錯誤" + "value": "Частный адрес недоступен" } }, - "ko": { + "th": { "stringUnit": { "state": "translated", - "value": "인증 오류" + "value": "ไม่มีที่อยู่ส่วนตัว" } - } - } - }, - "agent.claude.stopFailure.status.billing": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "tr": { "stringUnit": { "state": "translated", - "value": "Billing error" + "value": "Özel adres kullanılamıyor" } }, - "ja": { + "uk": { "stringUnit": { "state": "translated", - "value": "請求エラー" + "value": "Приватна адреса недоступна" } - }, - "zh-Hans": { + } + } + }, + "cloudTree.ports.privateAddress.detail": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "账单错误" + "value": "cmux’s authenticated route needs this machine’s private address. Refresh to retrieve it. Turning on Cloud VPN does not assign a missing address." } }, "de": { "stringUnit": { "state": "translated", - "value": "Abrechnungsfehler" + "value": "Die authentifizierte Route von cmux benötigt die private Adresse dieser Maschine. Aktualisieren Sie, um sie abzurufen. Cloud-VPN weist keine fehlende Adresse zu." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Erreur de facturation" + "value": "La route authentifiée de cmux nécessite l’adresse privée de cette machine. Actualisez pour la récupérer. Activer le VPN Cloud n’attribue pas d’adresse manquante." } }, "ar": { "stringUnit": { "state": "translated", - "value": "خطأ في الفوترة" + "value": "يحتاج مسار cmux الموثّق إلى العنوان الخاص لهذا الجهاز. حدّث لاسترجاعه. تشغيل Cloud VPN لا يعيّن عنوانًا مفقودًا." } }, "es": { "stringUnit": { "state": "translated", - "value": "Error de facturación" + "value": "La ruta autenticada de cmux necesita la dirección privada de esta máquina. Actualiza para obtenerla. Activar Cloud VPN no asigna una dirección que falta." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "帳單錯誤" + "value": "cmux 的已驗證路徑需要此機器的私人位址。請重新整理以取得位址。開啟 Cloud VPN 不會指派缺少的位址。" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "결제 오류" + "value": "cmux 的已验证路由需要此机器的专用地址。请刷新以获取地址。开启 Cloud VPN 不会分配缺失的地址。" } - } - } - }, - "agent.claude.stopFailure.status.connectionDropped": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ko": { "stringUnit": { "state": "translated", - "value": "Connection dropped" + "value": "cmux의 인증된 경로에는 이 머신의 비공개 주소가 필요합니다. 새로 고쳐 주소를 가져오세요. Cloud VPN을 켜도 누락된 주소가 할당되지는 않습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "接続が切断されました" + "value": "cmux の認証済み経路にはこのマシンのプライベートアドレスが必要です。更新して取得してください。Cloud VPN を有効にしても不足しているアドレスは割り当てられません。" } }, - "zh-Hans": { + "bs": { "stringUnit": { "state": "translated", - "value": "连接已断开" + "value": "Autentificirana ruta cmux-a treba privatnu adresu ove mašine. Osvježite da je preuzmete. Uključivanje Cloud VPN-a ne dodjeljuje adresu koja nedostaje." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "Verbindung abgebrochen" + "value": "cmux’ godkendte rute kræver denne maskines private adresse. Opdater for at hente den. Aktivering af Cloud VPN tildeler ikke en manglende adresse." } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "Connexion interrompue" + "value": "Il percorso autenticato di cmux richiede l’indirizzo privato di questa macchina. Aggiorna per recuperarlo. L’attivazione della VPN Cloud non assegna un indirizzo mancante." } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "انقطع الاتصال" + "value": "ផ្លូវដែលបានផ្ទៀងផ្ទាត់របស់ cmux ត្រូវការអាសយដ្ឋានឯកជនរបស់ម៉ាស៊ីននេះ។ ផ្ទុកឡើងវិញដើម្បីទាញយកវា។ ការបើក Cloud VPN មិនផ្តល់អាសយដ្ឋានដែលបាត់ទេ។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Conexión perdida" + "value": "cmux sin autentiserte rute trenger denne maskinens private adresse. Oppdater for å hente den. Å slå på Cloud VPN tildeler ikke en manglende adresse." } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "連線已中斷" + "value": "Uwierzytelniona trasa cmux wymaga prywatnego adresu tej maszyny. Odśwież, aby go pobrać. Włączenie Cloud VPN nie przypisuje brakującego adresu." } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "연결 끊김" + "value": "A rota autenticada do cmux precisa do endereço privado desta máquina. Atualize para obtê-lo. Ativar o Cloud VPN não atribui um endereço ausente." } - } - } - }, - "agent.claude.stopFailure.status.outputLimit": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ru": { "stringUnit": { "state": "translated", - "value": "Output limit" + "value": "Аутентифицированному маршруту cmux нужен частный адрес этой машины. Обновите, чтобы получить его. Включение Cloud VPN не назначает отсутствующий адрес." } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "出力上限" + "value": "เส้นทางที่ยืนยันตัวตนแล้วของ cmux ต้องใช้ที่อยู่ส่วนตัวของเครื่องนี้ รีเฟรชเพื่อดึงข้อมูล การเปิด Cloud VPN ไม่ได้กำหนดที่อยู่ที่ขาดหายไป" } }, - "zh-Hans": { + "tr": { "stringUnit": { "state": "translated", - "value": "输出上限" + "value": "cmux’un kimliği doğrulanmış yolu bu makinenin özel adresine ihtiyaç duyar. Adresi almak için yenileyin. Cloud VPN’i açmak eksik bir adres atamaz." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Автентифікованому маршруту cmux потрібна приватна адреса цієї машини. Оновіть, щоб отримати її. Увімкнення Cloud VPN не призначає відсутню адресу." + } + } + } + }, + "cloudTree.ports.routeNote": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux forwards ports without Cloud VPN. Web browsers and other Mac apps need Cloud VPN to reach services bound to the machine’s private address." } }, "de": { "stringUnit": { "state": "translated", - "value": "Ausgabelimit" + "value": "cmux leitet Ports ohne Cloud-VPN weiter. Webbrowser und andere Mac-Apps benötigen Cloud-VPN, um Dienste an der privaten Adresse der Maschine zu erreichen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Limite de sortie" + "value": "cmux transfère les ports sans VPN Cloud. Les navigateurs web et les autres apps Mac ont besoin du VPN Cloud pour atteindre les services liés à l’adresse privée de la machine." } }, "ar": { "stringUnit": { "state": "translated", - "value": "حد المخرجات" + "value": "يعيد cmux توجيه المنافذ دون Cloud VPN. تحتاج متصفحات الويب وتطبيقات Mac الأخرى إلى Cloud VPN للوصول إلى الخدمات المرتبطة بالعنوان الخاص للجهاز." } }, "es": { "stringUnit": { "state": "translated", - "value": "Límite de salida" + "value": "cmux reenvía puertos sin Cloud VPN. Los navegadores web y otras apps de Mac necesitan Cloud VPN para acceder a servicios enlazados a la dirección privada de la máquina." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "輸出上限" + "value": "cmux 可轉送連接埠,不需要 Cloud VPN。網頁瀏覽器和其他 Mac App 需要 Cloud VPN,才能存取繫結至機器私人位址的服務。" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "출력 한도" + "value": "cmux 可转发端口,无需 Cloud VPN。网页浏览器和其他 Mac 应用需要 Cloud VPN,才能访问绑定到机器专用地址的服务。" } - } - } - }, - "agent.claude.stopFailure.status.overloaded": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ko": { "stringUnit": { "state": "translated", - "value": "API overloaded" + "value": "cmux는 Cloud VPN 없이 포트를 전달합니다. 웹 브라우저 및 다른 Mac 앱이 머신의 비공개 주소에 바인딩된 서비스에 접근하려면 Cloud VPN이 필요합니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "API 過負荷" + "value": "cmux は Cloud VPN なしでポートを転送します。Web ブラウザやその他の Mac アプリからマシンのプライベートアドレスにバインドされたサービスにアクセスするには Cloud VPN が必要です。" } }, - "zh-Hans": { + "bs": { "stringUnit": { "state": "translated", - "value": "API 过载" + "value": "cmux prosljeđuje portove bez Cloud VPN-a. Web preglednicima i drugim Mac aplikacijama potreban je Cloud VPN za pristup servisima vezanim za privatnu adresu mašine." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "API überlastet" + "value": "cmux videresender porte uden Cloud VPN. Webbrowsere og andre Mac-apps skal bruge Cloud VPN for at nå tjenester, der er bundet til maskinens private adresse." } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "API surchargée" + "value": "cmux inoltra le porte senza VPN Cloud. I browser web e le altre app per Mac hanno bisogno della VPN Cloud per raggiungere i servizi associati all’indirizzo privato della macchina." } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "واجهة API محمّلة فوق طاقتها" + "value": "cmux បញ្ជូនបន្តច្រកដោយមិនត្រូវការ Cloud VPN។ កម្មវិធីរុករកបណ្តាញ និងកម្មវិធី Mac ផ្សេងទៀតត្រូវការ Cloud VPN ដើម្បីចូលដល់សេវាកម្មដែលចងទៅអាសយដ្ឋានឯកជនរបស់ម៉ាស៊ីន។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "API sobrecargada" + "value": "cmux videresender porter uten Cloud VPN. Nettlesere og andre Mac-apper trenger Cloud VPN for å nå tjenester som er bundet til maskinens private adresse." } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "API 過載" + "value": "cmux przekierowuje porty bez Cloud VPN. Przeglądarki internetowe i inne aplikacje na Maca potrzebują Cloud VPN, aby dotrzeć do usług powiązanych z prywatnym adresem maszyny." } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "API 과부하" + "value": "O cmux encaminha portas sem o Cloud VPN. Navegadores e outros apps do Mac precisam do Cloud VPN para acessar serviços vinculados ao endereço privado da máquina." } - } - } - }, - "agent.claude.stopFailure.status.rateLimited": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ru": { "stringUnit": { "state": "translated", - "value": "Rate limited" + "value": "cmux перенаправляет порты без Cloud VPN. Веб-браузерам и другим приложениям Mac нужен Cloud VPN, чтобы обращаться к службам, привязанным к частному адресу машины." } }, - "ja": { + "th": { "stringUnit": { "state": "translated", - "value": "レート制限中" + "value": "cmux ส่งต่อพอร์ตได้โดยไม่ต้องใช้ Cloud VPN เว็บเบราว์เซอร์และแอป Mac อื่นๆ ต้องใช้ Cloud VPN เพื่อเข้าถึงบริการที่ผูกกับที่อยู่ส่วนตัวของเครื่อง" } }, - "zh-Hans": { + "tr": { "stringUnit": { "state": "translated", - "value": "已限速" + "value": "cmux, portları Cloud VPN olmadan yönlendirir. Web tarayıcıları ve diğer Mac uygulamaları, makinenin özel adresine bağlı hizmetlere ulaşmak için Cloud VPN’e ihtiyaç duyar." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "cmux перенаправляє порти без Cloud VPN. Веббраузерам та іншим застосункам Mac потрібен Cloud VPN, щоб звертатися до служб, прив’язаних до приватної адреси машини." + } + } + } + }, + "cloudTree.ports.setupVPN": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Set Up VPN…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ratenbegrenzt" + "value": "VPN einrichten…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Limite de requêtes atteinte" + "value": "Configurer le VPN…" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تم تقييد المعدل" + "value": "إعداد VPN…" } }, "es": { "stringUnit": { "state": "translated", - "value": "Límite de frecuencia alcanzado" + "value": "Configurar VPN…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已限速" + "value": "設定 VPN…" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "속도 제한됨" + "value": "设置 VPN…" } - } - } - }, - "agent.claude.stopFailure.status.usageLimit": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ko": { "stringUnit": { "state": "translated", - "value": "Usage limit" + "value": "VPN 설정…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "使用量の上限" + "value": "VPN を設定…" } }, - "zh-Hans": { + "bs": { "stringUnit": { "state": "translated", - "value": "用量上限" + "value": "Postavi VPN…" } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "Nutzungslimit" + "value": "Konfigurer VPN…" } }, - "fr": { + "it": { "stringUnit": { "state": "translated", - "value": "Limite d'utilisation" + "value": "Configura VPN…" } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "حد الاستخدام" + "value": "រៀបចំ VPN…" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Límite de uso" + "value": "Konfigurer VPN…" } }, - "zh-Hant": { + "pl": { "stringUnit": { "state": "translated", - "value": "用量上限" + "value": "Skonfiguruj VPN…" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사용량 한도" - } - } - } - }, - "agent.claude.stopFailure.status.usageLimitResets": { - "extractionState": "manual", - "localizations": { - "en": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Usage limit, resets %@" + "value": "Configurar VPN…" } }, - "ja": { + "ru": { "stringUnit": { "state": "translated", - "value": "使用量の上限、%@ にリセット" + "value": "Настроить VPN…" } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "用量上限,%@ 重置" + "value": "ตั้งค่า VPN…" } }, - "de": { + "tr": { "stringUnit": { "state": "translated", - "value": "Nutzungslimit, Reset %@" + "value": "VPN’i Kur…" } }, - "fr": { + "uk": { "stringUnit": { "state": "translated", - "value": "Limite d'utilisation, réinitialisation %@" + "value": "Налаштувати VPN…" } - }, - "ar": { + } + } + }, + "cloudTree.ports.stale": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "حد الاستخدام، يُعاد التعيين %@" + "value": "Port list may be out of date" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Límite de uso, se restablece %@" + "value": "Portliste möglicherweise veraltet" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "用量上限,%@ 重設" + "value": "La liste des ports peut être obsolète" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사용량 한도, %@에 초기화" - } - } - } - }, - "settings.app.reorderOnNotification.mode.agentActivity": { - "extractionState": "manual", - "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "نشاط الوكيل" + "value": "قد تكون قائمة المنافذ قديمة" } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "Aktivnost agenata" + "value": "La lista de puertos puede estar desactualizada" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Agentaktivitet" + "value": "連接埠清單可能已過時" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Agentenaktivität" + "value": "端口列表可能已过期" } }, - "en": { + "ko": { "stringUnit": { "state": "translated", - "value": "Agent Activity" + "value": "포트 목록이 오래되었을 수 있음" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Actividad de agentes" + "value": "ポートリストが古い可能性があります" } }, - "fr": { + "bs": { "stringUnit": { "state": "translated", - "value": "Activité des agents" + "value": "Lista portova je možda zastarjela" } }, - "it": { + "da": { "stringUnit": { "state": "translated", - "value": "Attività degli agenti" + "value": "Portlisten er muligvis forældet" } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "エージェントのアクティビティ" + "value": "L’elenco delle porte potrebbe non essere aggiornato" } }, - "ko": { + "km": { "stringUnit": { "state": "translated", - "value": "에이전트 활동" + "value": "បញ្ជីច្រកអាចហួសសម័យ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Agentaktivitet" + "value": "Portlisten kan være utdatert" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Aktywność agentów" + "value": "Lista portów może być nieaktualna" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Atividade dos agentes" + "value": "A lista de portas pode estar desatualizada" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Активность агентов" + "value": "Список портов может быть устаревшим" } }, "th": { "stringUnit": { "state": "translated", - "value": "กิจกรรมของเอเจนต์" + "value": "รายการพอร์ตอาจไม่เป็นปัจจุบัน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Ajan etkinliği" + "value": "Port listesi güncel olmayabilir" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Активність агентів" + "value": "Список портів може бути застарілим" + } + } + } + }, + "cloudTree.ports.stale.detail": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The last scan could not be refreshed. Listed services may have changed; refresh to check again. cmux forwarding does not need Cloud VPN." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "代理活动" + "value": "Der letzte Scan konnte nicht aktualisiert werden. Die aufgeführten Dienste könnten sich geändert haben. Aktualisieren Sie erneut. Die cmux-Weiterleitung benötigt kein Cloud-VPN." } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "代理活動" + "value": "La dernière analyse n’a pas pu être actualisée. Les services listés peuvent avoir changé ; actualisez pour vérifier. Le transfert cmux ne nécessite pas de VPN Cloud." } - } - } - }, - "settings.app.reorderOnNotification.mode.notifications": { - "extractionState": "manual", - "localizations": { + }, "ar": { "stringUnit": { "state": "translated", - "value": "الإشعارات" + "value": "تعذر تحديث آخر فحص. ربما تغيرت الخدمات المدرجة؛ حدّث للفحص مجددًا. لا تحتاج إعادة توجيه cmux إلى Cloud VPN." } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "Obavještenja" + "value": "No se pudo actualizar el último escaneo. Los servicios listados pueden haber cambiado; actualiza para comprobarlos. El reenvío de cmux no necesita Cloud VPN." } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Notifikationer" + "value": "無法重新整理上次掃描結果。列出的服務可能已變更;請重新整理以再次檢查。cmux 轉送不需要 Cloud VPN。" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Benachrichtigungen" + "value": "无法刷新上次扫描结果。列出的服务可能已更改;请刷新以再次检查。cmux 转发无需 Cloud VPN。" } }, - "en": { + "ko": { "stringUnit": { "state": "translated", - "value": "Notifications" + "value": "마지막 검색 결과를 새로 고치지 못했습니다. 나열된 서비스가 변경되었을 수 있으니 새로 고쳐 확인하세요. cmux 전달에는 Cloud VPN이 필요하지 않습니다." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Notificaciones" + "value": "前回のスキャンを更新できませんでした。表示されたサービスが変わっている可能性があります。更新して再確認してください。cmux の転送に Cloud VPN は不要です。" } }, - "fr": { + "bs": { "stringUnit": { "state": "translated", - "value": "Sur notification" + "value": "Posljednje skeniranje nije se moglo osvježiti. Navedeni servisi su se možda promijenili; osvježite da ponovo provjerite. Prosljeđivanje u cmux-u ne treba Cloud VPN." } }, - "it": { + "da": { "stringUnit": { "state": "translated", - "value": "Notifiche" + "value": "Den seneste scanning kunne ikke opdateres. De viste tjenester kan have ændret sig; opdater for at kontrollere igen. Videresendelse i cmux kræver ikke Cloud VPN." } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "通知" + "value": "Non è stato possibile aggiornare l’ultima scansione. I servizi elencati potrebbero essere cambiati; aggiorna per controllare di nuovo. L’inoltro di cmux non richiede la VPN Cloud." } }, - "ko": { + "km": { "stringUnit": { "state": "translated", - "value": "알림" + "value": "មិនអាចផ្ទុកការស្កេនចុងក្រោយឡើងវិញបានទេ។ សេវាកម្មដែលបានរាយអាចបានផ្លាស់ប្តូរ; ផ្ទុកឡើងវិញដើម្បីពិនិត្យម្តងទៀត។ ការបញ្ជូនបន្តរបស់ cmux មិនត្រូវការ Cloud VPN ទេ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Varsler" + "value": "Den siste skanningen kunne ikke oppdateres. De oppførte tjenestene kan ha endret seg; oppdater for å sjekke på nytt. Videresending i cmux trenger ikke Cloud VPN." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Powiadomienia" + "value": "Nie udało się odświeżyć ostatniego skanowania. Wymienione usługi mogły się zmienić; odśwież, aby sprawdzić ponownie. Przekierowywanie w cmux nie wymaga Cloud VPN." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Notificações" + "value": "Não foi possível atualizar a última verificação. Os serviços listados podem ter mudado; atualize para verificar de novo. O encaminhamento do cmux não precisa do Cloud VPN." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Уведомления" + "value": "Не удалось обновить последнее сканирование. Перечисленные службы могли измениться; обновите, чтобы проверить снова. Перенаправлению cmux Cloud VPN не нужен." } }, "th": { "stringUnit": { "state": "translated", - "value": "การแจ้งเตือน" + "value": "ไม่สามารถรีเฟรชการสแกนครั้งล่าสุดได้ บริการที่แสดงอาจเปลี่ยนไปแล้ว รีเฟรชเพื่อตรวจสอบอีกครั้ง การส่งต่อของ cmux ไม่ต้องใช้ Cloud VPN" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bildirimler" + "value": "Son tarama yenilenemedi. Listelenen hizmetler değişmiş olabilir; yeniden denetlemek için yenileyin. cmux yönlendirmesi Cloud VPN gerektirmez." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Сповіщення" + "value": "Не вдалося оновити останнє сканування. Перелічені служби могли змінитися; оновіть, щоб перевірити знову. Перенаправлення cmux не потребує Cloud VPN." + } + } + } + }, + "cloudTree.ports.unavailable.detail": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The port scan failed; this does not mean no service is listening. Existing cmux ports may still work. Refresh to retry; Cloud VPN is not required." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "通知" + "value": "Der Portscan ist fehlgeschlagen; das bedeutet nicht, dass kein Dienst lauscht. Vorhandene cmux-Ports funktionieren möglicherweise weiter. Aktualisieren Sie; Cloud-VPN ist nicht erforderlich." } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "通知" + "value": "L’analyse des ports a échoué ; cela ne signifie pas qu’aucun service n’écoute. Les ports cmux existants peuvent encore fonctionner. Actualisez pour réessayer ; le VPN Cloud n’est pas nécessaire." } - } - } - }, - "settings.app.reorderOnNotification.mode.off": { - "extractionState": "manual", - "localizations": { + }, "ar": { "stringUnit": { "state": "translated", - "value": "إيقاف" + "value": "فشل فحص المنافذ؛ هذا لا يعني عدم وجود خدمة تستمع. قد تظل منافذ cmux الحالية تعمل. حدّث للمحاولة مجددًا؛ Cloud VPN غير مطلوب." } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "Isključeno" + "value": "Falló el escaneo de puertos; no significa que no haya servicios en escucha. Los puertos de cmux existentes pueden seguir funcionando. Actualiza para reintentar; Cloud VPN no es necesario." } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Fra" + "value": "連接埠掃描失敗;這不代表沒有服務正在監聽。現有 cmux 連接埠可能仍可使用。請重新整理以重試;不需要 Cloud VPN。" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Aus" + "value": "端口扫描失败;这并不代表没有服务正在监听。现有 cmux 端口可能仍可使用。请刷新以重试;无需 Cloud VPN。" } }, - "en": { + "ko": { "stringUnit": { "state": "translated", - "value": "Off" + "value": "포트 검색이 실패했지만 수신 대기 서비스가 없다는 뜻은 아닙니다. 기존 cmux 포트는 계속 작동할 수 있습니다. 새로 고쳐 다시 시도하세요. Cloud VPN은 필요하지 않습니다." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Desactivado" + "value": "ポートスキャンが失敗しましたが、サービスが待ち受けていないとは限りません。既存の cmux ポートは引き続き動作する可能性があります。更新して再試行してください。Cloud VPN は不要です。" } }, - "fr": { + "bs": { "stringUnit": { "state": "translated", - "value": "Désactivé" + "value": "Skeniranje portova nije uspjelo; to ne znači da nijedan servis ne sluša. Postojeći cmux portovi možda i dalje rade. Osvježite za ponovni pokušaj; Cloud VPN nije potreban." } }, - "it": { + "da": { "stringUnit": { "state": "translated", - "value": "Disattivato" + "value": "Portscanningen mislykkedes; det betyder ikke, at ingen tjeneste lytter. Eksisterende cmux-porte virker muligvis stadig. Opdater for at prøve igen; Cloud VPN er ikke påkrævet." } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "オフ" + "value": "La scansione delle porte non è riuscita; ciò non significa che nessun servizio sia in ascolto. Le porte cmux esistenti potrebbero funzionare ancora. Aggiorna per riprovare; la VPN Cloud non è necessaria." } }, - "ko": { + "km": { "stringUnit": { "state": "translated", - "value": "끔" + "value": "ការស្កេនច្រកបានបរាជ័យ; នេះមិនមានន័យថាគ្មានសេវាកម្មណាកំពុងស្តាប់ទេ។ ច្រក cmux ដែលមានស្រាប់អាចនៅតែដំណើរការ។ ផ្ទុកឡើងវិញដើម្បីព្យាយាមម្តងទៀត; មិនត្រូវការ Cloud VPN ទេ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Av" + "value": "Portskanningen mislyktes; det betyr ikke at ingen tjeneste lytter. Eksisterende cmux-porter kan fortsatt fungere. Oppdater for å prøve igjen; Cloud VPN er ikke nødvendig." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Wyłączone" + "value": "Skanowanie portów nie powiodło się; nie oznacza to, że żadna usługa nie nasłuchuje. Istniejące porty cmux mogą nadal działać. Odśwież, aby spróbować ponownie; Cloud VPN nie jest wymagany." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Desativado" + "value": "A verificação de portas falhou; isso não significa que nenhum serviço esteja em escuta. As portas existentes do cmux ainda podem funcionar. Atualize para tentar de novo; o Cloud VPN não é necessário." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Выкл." + "value": "Сканирование портов не удалось; это не значит, что ни одна служба не прослушивает порты. Существующие порты cmux могут по-прежнему работать. Обновите, чтобы повторить; Cloud VPN не требуется." } }, "th": { "stringUnit": { "state": "translated", - "value": "ปิด" + "value": "การสแกนพอร์ตล้มเหลว ไม่ได้หมายความว่าไม่มีบริการที่กำลังรับฟัง พอร์ต cmux ที่มีอยู่อาจยังใช้งานได้ รีเฟรชเพื่อลองอีกครั้ง ไม่จำเป็นต้องใช้ Cloud VPN" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Kapalı" + "value": "Port taraması başarısız oldu; bu, dinleyen bir hizmet olmadığı anlamına gelmez. Mevcut cmux portları hâlâ çalışıyor olabilir. Yeniden denemek için yenileyin; Cloud VPN gerekmez." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Вимкнено" + "value": "Сканування портів не вдалося; це не означає, що жодна служба не прослуховує. Наявні порти cmux можуть і далі працювати. Оновіть, щоб повторити; Cloud VPN не потрібен." + } + } + } + }, + "cloudTree.ports.unsupported": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ports are not supported by this provider." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "关闭" + "value": "Ports werden von diesem Anbieter nicht unterstützt." } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "關閉" + "value": "Les ports ne sont pas pris en charge par ce fournisseur." } - } - } - }, - "settings.app.reorderOnNotification.modeSubtitle": { - "extractionState": "manual", - "localizations": { + }, "ar": { "stringUnit": { "state": "translated", - "value": "ينقل مساحات العمل إلى الأعلى عندما تتلقى إشعارًا. مع نشاط الوكيل، ينقلها أيضًا عندما ترسل مطالبة أو ينهي وكيل دورة أو يحتاج إلى إدخال أو يفشل، لكن ليس أبدًا أثناء وجود المؤشر فوق الشريط الجانبي. الإيقاف يحافظ على ثبات مواضع الاختصارات." + "value": "لا يدعم هذا الموفر المنافذ." } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "Pomjera radne prostore na vrh kada prime obavještenje. Aktivnost agenata ih pomjera i kada pošaljete upit ili agent završi potez, treba unos ili ne uspije, ali nikad dok je pokazivač iznad bočne trake. Isključeno čuva stabilne pozicije prečica." + "value": "Este proveedor no admite puertos." } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Flytter arbejdsområder til toppen, når de modtager en notifikation. Agentaktivitet flytter dem også, når du sender en prompt eller en agent afslutter en tur, har brug for input eller fejler, men aldrig mens markøren er over sidepanelet. Fra holder genvejspositionerne stabile." + "value": "此提供者不支援連接埠。" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Verschiebt Workspaces nach oben, wenn sie eine Benachrichtigung erhalten. Mit Agentenaktivität auch dann, wenn Sie einen Prompt senden oder ein Agent einen Durchlauf beendet, eine Eingabe braucht oder fehlschlägt, aber nie, während der Zeiger über der Seitenleiste ist. Aus hält die Positionen der Tastenkürzel stabil." + "value": "此提供商不支持端口。" } }, - "en": { + "ko": { "stringUnit": { "state": "translated", - "value": "Move workspaces to the top when they receive a notification. Agent Activity also moves them when you send a prompt or an agent finishes a turn, needs input, or fails, but never while you point at the sidebar. Off keeps shortcut positions stable." + "value": "이 제공자는 포트를 지원하지 않습니다." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Mueve los espacios de trabajo arriba cuando reciben una notificación. Con Actividad de agentes, también cuando envías un prompt o un agente termina un turno, necesita una respuesta o falla, pero nunca mientras el puntero está sobre la barra lateral. Desactivado mantiene estables las posiciones de los atajos." + "value": "このプロバイダーはポートをサポートしていません。" } }, - "fr": { + "bs": { "stringUnit": { "state": "translated", - "value": "Place les espaces de travail en haut lorsqu’ils reçoivent une notification. Avec Activité des agents, aussi lorsque vous envoyez un prompt ou qu’un agent termine un tour, attend une saisie ou échoue, mais jamais pendant que le pointeur survole la barre latérale. Désactivé garde les positions des raccourcis stables." + "value": "Ovaj pružalac ne podržava portove." } }, - "it": { + "da": { "stringUnit": { "state": "translated", - "value": "Sposta gli spazi di lavoro in cima quando ricevono una notifica. Attività degli agenti li sposta anche quando invii un prompt o un agente termina un turno, attende un input o fallisce, ma mai mentre il puntatore è sulla barra laterale. Disattivato mantiene stabili le posizioni delle scorciatoie." + "value": "Porte understøttes ikke af denne udbyder." } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "通知を受け取ったワークスペースを先頭に移動します。「エージェントのアクティビティ」では、プロンプトを送信したときや、エージェントがターンを終えたとき、入力を待っているとき、失敗したときにも移動しますが、ポインタがサイドバー上にある間は移動しません。「オフ」ではショートカットの位置が変わりません。" + "value": "Le porte non sono supportate da questo provider." } }, - "ko": { + "km": { "stringUnit": { "state": "translated", - "value": "알림을 받으면 워크스페이스를 맨 위로 옮깁니다. 에이전트 활동을 선택하면 프롬프트를 보내거나 에이전트가 턴을 마치거나 입력이 필요하거나 실패할 때도 옮기지만, 포인터가 사이드바 위에 있을 때는 옮기지 않습니다. 끔은 단축키 위치를 고정합니다." + "value": "អ្នកផ្តល់សេវានេះមិនគាំទ្រច្រកទេ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Flytter arbeidsområder til toppen når de mottar et varsel. Agentaktivitet flytter dem også når du sender en ledetekst eller en agent fullfører en runde, trenger input eller feiler, men aldri mens pekeren er over sidefeltet. Av holder snarveisposisjonene stabile." + "value": "Porter støttes ikke av denne leverandøren." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Przenosi obszary robocze na górę, gdy otrzymają powiadomienie. Aktywność agentów przenosi je także, gdy wyślesz prompt lub agent zakończy turę, potrzebuje danych lub zakończy się błędem, ale nigdy, gdy wskaźnik jest nad paskiem bocznym. Wyłączone zachowuje stałe pozycje skrótów." + "value": "Ten dostawca nie obsługuje portów." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Move os espaços de trabalho para o topo quando recebem uma notificação. Atividade dos agentes também os move quando você envia um prompt ou um agente termina um turno, precisa de uma resposta ou falha, mas nunca enquanto o ponteiro está sobre a barra lateral. Desativado mantém as posições dos atalhos estáveis." + "value": "Este provedor não oferece suporte a portas." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Перемещает рабочие области наверх, когда они получают уведомление. «Активность агентов» также перемещает их, когда вы отправляете запрос или агент завершает ход, ждёт ввода или завершается с ошибкой, но никогда, пока указатель находится над боковой панелью. «Выкл.» сохраняет позиции сочетаний клавиш." + "value": "Этот провайдер не поддерживает порты." } }, "th": { "stringUnit": { "state": "translated", - "value": "ย้ายเวิร์กสเปซขึ้นด้านบนเมื่อได้รับการแจ้งเตือน กิจกรรมของเอเจนต์จะย้ายเมื่อคุณส่งพรอมต์หรือเอเจนต์จบรอบ ต้องการข้อมูล หรือล้มเหลวด้วย แต่จะไม่ย้ายขณะที่ตัวชี้อยู่เหนือแถบด้านข้าง ปิดจะคงตำแหน่งของทางลัดไว้" + "value": "ผู้ให้บริการนี้ไม่รองรับพอร์ต" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bildirim aldıklarında çalışma alanlarını en üste taşır. Ajan etkinliği, bir istem gönderdiğinizde, bir ajan turunu bitirdiğinde, girdi beklediğinde veya başarısız olduğunda da taşır, ancak işaretçi kenar çubuğunun üzerindeyken asla taşımaz. Kapalı, kısayol konumlarını sabit tutar." + "value": "Bu sağlayıcı portları desteklemiyor." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Переміщує робочі області вгору, коли вони отримують сповіщення. «Активність агентів» також переміщує їх, коли ви надсилаєте запит або агент завершує хід, чекає на введення або завершується з помилкою, але ніколи, поки вказівник над бічною панеллю. «Вимкнено» зберігає позиції скорочень." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "工作区收到通知时移到顶部。选择“代理活动”时,你发送提示词或代理完成一轮、需要输入或失败时也会移动,但指针停在侧边栏上时绝不移动。关闭可让快捷键位置保持不变。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "工作區收到通知時移到頂部。選擇「代理活動」時,你送出提示詞或代理完成一輪、需要輸入或失敗時也會移動,但指標停在側邊欄上時絕不移動。關閉可讓快捷鍵位置保持不變。" + "value": "Цей постачальник не підтримує порти." } } } }, - "cli.restore.notice.routedLauncherUnavailable": { + "cloudTree.ports.unsupported.detail": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "restore: this session was started through the routed launcher '%@', which is not on PATH. Resuming the agent directly instead." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "restore: このセッションはルーティングされたランチャー「%@」経由で開始されましたが、PATH にありません。代わりにエージェントを直接再開します。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "restore:此会话是通过路由启动器“%@”启动的,但它不在 PATH 中。将改为直接恢复该代理。" + "value": "This provider has no supported in-app port route. Open a shell to access the service inside the machine. Cloud VPN does not add provider support." } }, "de": { "stringUnit": { "state": "translated", - "value": "restore: Diese Sitzung wurde über den weitergeleiteten Launcher „%@“ gestartet, der nicht im PATH liegt. Der Agent wird stattdessen direkt fortgesetzt." + "value": "Dieser Anbieter unterstützt keine Portverbindung in der App. Öffnen Sie eine Shell, um innerhalb der Maschine auf den Dienst zuzugreifen. Cloud-VPN fügt keine Anbieterunterstützung hinzu." } }, "fr": { "stringUnit": { "state": "translated", - "value": "restore : cette session a été lancée via le lanceur routé « %@ », qui est introuvable dans le PATH. Reprise directe de l'agent à la place." + "value": "Ce fournisseur ne prend pas en charge les ports dans l’app. Ouvrez un shell pour accéder au service dans la machine. Le VPN Cloud n’ajoute pas cette prise en charge." } }, "ar": { "stringUnit": { "state": "translated", - "value": "restore: بدأت هذه الجلسة عبر المشغّل الموجَّه '%@'، وهو غير موجود في PATH. سيتم استئناف الوكيل مباشرةً بدلاً من ذلك." + "value": "لا يدعم هذا الموفر مسار منافذ داخل التطبيق. افتح صدفة للوصول إلى الخدمة داخل الجهاز. لا يضيف Cloud VPN دعمًا للموفر." } }, "es": { "stringUnit": { "state": "translated", - "value": "restore: esta sesión se inició mediante el lanzador enrutado '%@', que no está en el PATH. Se reanudará el agente directamente." + "value": "Este proveedor no admite una ruta de puertos dentro de la app. Abre una shell para acceder al servicio dentro de la máquina. Cloud VPN no añade soporte al proveedor." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "restore:此工作階段是透過路由啟動器「%@」啟動的,但它不在 PATH 中。將改為直接繼續該代理程式。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "restore: 이 세션은 라우팅된 런처 '%@'를 통해 시작되었지만 PATH에 없습니다. 대신 에이전트를 직접 재개합니다." - } - } - } - }, - "sidebar.extensions.action.noSpaceForPane": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "No space for new pane" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لا توجد مساحة للوحة جديدة" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kein Platz für einen neuen Bereich" + "value": "此提供者不支援應用程式內連接埠路徑。請開啟 Shell 以在機器內存取服務。Cloud VPN 不會新增提供者支援。" } }, - "es": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "No hay espacio para un panel nuevo" + "value": "此提供商不支持应用内端口路由。请打开 Shell 以在机器内部访问服务。Cloud VPN 不会添加提供商支持。" } }, - "fr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Pas de place pour un nouveau panneau" + "value": "이 제공자는 앱 내 포트 경로를 지원하지 않습니다. 셸을 열어 머신 내부의 서비스에 접근하세요. Cloud VPN으로 제공자 지원이 추가되지는 않습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "新しいペインを置く場所がありません" + "value": "このプロバイダーはアプリ内のポート経路に対応していません。シェルを開き、マシン内でサービスにアクセスしてください。Cloud VPN を有効にしても対応は追加されません。" } }, - "ko": { + "bs": { "stringUnit": { "state": "translated", - "value": "새 창을 위한 공간이 없습니다" + "value": "Ovaj pružalac nema podržanu rutu portova unutar aplikacije. Otvorite shell da pristupite servisu unutar mašine. Cloud VPN ne dodaje podršku pružaoca." } }, - "zh-Hans": { + "da": { "stringUnit": { "state": "translated", - "value": "没有空间容纳新窗格" + "value": "Denne udbyder har ingen understøttet portrute i appen. Åbn en shell for at få adgang til tjenesten inde i maskinen. Cloud VPN tilføjer ikke understøttelse for udbyderen." } }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "沒有空間容納新窗格" - } - } - } - }, - "cli.focus.defaultHelp": { - "extractionState": "manual", - "localizations": { - "en": { + "it": { "stringUnit": { "state": "translated", - "value": "Default: focus when run interactively, stay in the background when run by an agent or script." + "value": "Questo provider non ha un percorso delle porte supportato nell’app. Apri una shell per accedere al servizio all’interno della macchina. La VPN Cloud non aggiunge il supporto del provider." } }, - "ja": { + "km": { "stringUnit": { "state": "translated", - "value": "既定: 対話的に実行した場合はフォーカスし、エージェントやスクリプトから実行した場合はバックグラウンドのままにします。" + "value": "អ្នកផ្តល់សេវានេះគ្មានផ្លូវច្រកក្នុងកម្មវិធីដែលគាំទ្រទេ។ បើក shell ដើម្បីចូលប្រើសេវាកម្មនៅខាងក្នុងម៉ាស៊ីន។ Cloud VPN មិនបន្ថែមការគាំទ្រអ្នកផ្តល់សេវាទេ។" } }, - "de": { + "nb": { "stringUnit": { "state": "translated", - "value": "Standard: bei interaktiver Ausführung fokussieren, bei Ausführung durch einen Agenten oder ein Skript im Hintergrund bleiben." + "value": "Denne leverandøren har ingen støttet portrute i appen. Åpne et skall for å få tilgang til tjenesten inne i maskinen. Cloud VPN legger ikke til støtte for leverandøren." } }, - "fr": { + "pl": { "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": "Ten dostawca nie ma obsługiwanej trasy portów w aplikacji. Otwórz powłokę, aby uzyskać dostęp do usługi wewnątrz maszyny. Cloud VPN nie dodaje obsługi dostawcy." } }, - "ar": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "الافتراضي: التركيز عند التشغيل التفاعلي، والبقاء في الخلفية عند التشغيل بواسطة وكيل أو برنامج نصي." + "value": "Este provedor não tem uma rota de portas compatível no app. Abra um shell para acessar o serviço dentro da máquina. O Cloud VPN não adiciona suporte ao provedor." } }, - "es": { + "ru": { "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": "У этого провайдера нет поддерживаемого маршрута портов в приложении. Откройте оболочку, чтобы получить доступ к службе внутри машины. Cloud VPN не добавляет поддержку провайдера." } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "預設:互動執行時取得焦點;由代理程式或指令稿執行時保持在背景。" + "value": "ผู้ให้บริการนี้ไม่มีเส้นทางพอร์ตในแอปที่รองรับ เปิดเชลล์เพื่อเข้าถึงบริการภายในเครื่อง Cloud VPN ไม่ได้เพิ่มการรองรับผู้ให้บริการ" } }, - "zh-Hans": { + "tr": { "stringUnit": { "state": "translated", - "value": "默认:交互式运行时获得焦点;由代理或脚本运行时保持在后台。" + "value": "Bu sağlayıcının desteklenen bir uygulama içi port yolu yok. Makinenin içindeki hizmete erişmek için bir kabuk açın. Cloud VPN sağlayıcı desteği eklemez." } }, - "ko": { + "uk": { "stringUnit": { "state": "translated", - "value": "기본값: 대화형으로 실행하면 포커스를 가져오고, 에이전트나 스크립트가 실행하면 백그라운드에 머뭅니다." + "value": "Цей постачальник не має підтримуваного маршруту портів у застосунку. Відкрийте оболонку, щоб отримати доступ до служби всередині машини. Cloud VPN не додає підтримку постачальника." } } } }, - "cli.help.openFocusFlags": { + "settings.automation.pi": { "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": "Pi Integration" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "フォーカス:\n --focus 新しいワークスペースに切り替えます\n --no-focus バックグラウンドで開きます" + "value": "تكامل Pi" } }, "de": { "stringUnit": { "state": "translated", - "value": "Fokus:\n --focus Zum neuen Workspace wechseln\n --no-focus Im Hintergrund öffnen" + "value": "Pi-Integration" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Integración de Pi" } }, "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": "Intégration de Pi" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "التركيز:\n --focus ينتقل إلى مساحة العمل الجديدة\n --no-focus يفتحها في الخلفية" + "value": "Pi連携" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Foco:\n --focus Cambia al nuevo espacio de trabajo\n --no-focus Lo abre en segundo plano" + "value": "Pi 연동" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "Pi 集成" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "焦點:\n --focus 切換到新的工作區\n --no-focus 在背景開啟" + "value": "Pi 整合" } }, - "zh-Hans": { + "bs": { "stringUnit": { "state": "translated", - "value": "焦点:\n --focus 切换到新的工作区\n --no-focus 在后台打开" + "value": "Pi integracija" } }, - "ko": { + "da": { "stringUnit": { "state": "translated", - "value": "포커스:\n --focus 새 워크스페이스로 전환합니다\n --no-focus 백그라운드에서 엽니다" + "value": "Pi-integration" } - } - } - }, - "settings.app.compactAgentStatus": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "it": { "stringUnit": { "state": "translated", - "value": "Compact Agent Status" + "value": "Integrazione di Pi" } }, - "ar": { + "km": { "stringUnit": { "state": "translated", - "value": "حالة الوكيل المختصرة" + "value": "ការរួមបញ្ចូល Pi" } }, - "de": { + "nb": { "stringUnit": { "state": "translated", - "value": "Kompakter Agentenstatus" + "value": "Pi-integrasjon" } }, - "es": { + "pl": { "stringUnit": { "state": "translated", - "value": "Estado de agente compacto" + "value": "Integracja Pi" } }, - "fr": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Statut d’agent compact" + "value": "Integração do Pi" } }, - "ja": { + "ru": { "stringUnit": { "state": "translated", - "value": "エージェント状態のコンパクト表示" + "value": "Интеграция Pi" } }, - "ko": { + "th": { "stringUnit": { "state": "translated", - "value": "간결한 에이전트 상태" + "value": "การผสานรวม Pi" } }, - "zh-Hans": { + "tr": { "stringUnit": { "state": "translated", - "value": "精简代理状态" + "value": "Pi entegrasyonu" } }, - "zh-Hant": { + "uk": { "stringUnit": { "state": "translated", - "value": "精簡代理狀態" + "value": "Інтеграція Pi" } } } }, - "settings.app.compactAgentStatus.subtitle": { + "settings.automation.pi.note": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Show a workspace's agent, unread and pull request state as one colored icon before the title instead of separate rows. Hover the icon for details." + "value": "When enabled, cmux wraps the pi command and loads its bundled session extension without changing your Pi configuration. Disable if you prefer to manage Pi hooks yourself." } }, "ar": { "stringUnit": { "state": "translated", - "value": "إظهار حالة الوكيل والعناصر غير المقروءة وطلب السحب لمساحة العمل كأيقونة ملونة واحدة قبل العنوان بدلاً من صفوف منفصلة. مرّر المؤشر فوق الأيقونة لعرض التفاصيل." + "value": "عند التفعيل، يغلّف cmux أمر pi ويحمّل امتداد الجلسة المضمّن دون تغيير إعدادات Pi. عطّل هذا الخيار إذا كنت تفضّل إدارة خطافات Pi بنفسك." } }, "de": { "stringUnit": { "state": "translated", - "value": "Agenten-, Ungelesen- und Pull-Request-Status eines Workspace als ein farbiges Symbol vor dem Titel anzeigen statt in eigenen Zeilen. Details erscheinen, wenn du den Zeiger über das Symbol bewegst." + "value": "Wenn aktiviert, umhüllt cmux den pi-Befehl und lädt seine gebündelte Sitzungserweiterung, ohne deine Pi-Konfiguration zu ändern. Deaktiviere dies, wenn du die Pi-Hooks selbst verwalten möchtest." } }, "es": { "stringUnit": { "state": "translated", - "value": "Mostrar el estado del agente, lo no leído y la pull request de un espacio de trabajo como un único icono de color antes del título, en lugar de filas separadas. Pasa el cursor sobre el icono para ver los detalles." + "value": "Cuando está activado, cmux envuelve el comando pi y carga su extensión de sesión integrada sin cambiar tu configuración de Pi. Desactívalo si prefieres gestionar tú mismo los hooks de Pi." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Afficher l’état de l’agent, des éléments non lus et de la pull request d’un espace de travail sous forme d’une seule icône colorée avant le titre, au lieu de lignes distinctes. Survolez l’icône pour voir les détails." + "value": "Lorsque cette option est activée, cmux encapsule la commande pi et charge son extension de session intégrée sans modifier votre configuration Pi. Désactivez-la si vous préférez gérer les hooks Pi vous-même." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ワークスペースのエージェント、未読、プルリクエストの状態を、個別の行ではなくタイトルの前の色付きアイコン1つで示します。詳細はアイコンにポインタを合わせると表示されます。" + "value": "有効にすると、cmuxはpiコマンドをラップし、Piの設定を変更せずにバンドルされたセッション拡張機能を読み込みます。Piのフックを自分で管理する場合は無効にしてください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "워크스페이스의 에이전트, 읽지 않음, 풀 리퀘스트 상태를 별도의 줄이 아니라 제목 앞의 색상 아이콘 하나로 나타냅니다. 자세한 내용은 아이콘 위에 포인터를 올려 확인하세요." + "value": "활성화하면 cmux가 pi 명령을 래핑하고 Pi 설정을 변경하지 않고 번들 세션 확장을 로드합니다. Pi 훅을 직접 관리하려면 비활성화하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "用标题前的单个彩色图标表示工作区的代理、未读和拉取请求状态,而不是各自占一行。将指针悬停在图标上可查看详细信息。" + "value": "启用后,cmux 会包装 pi 命令并加载内置会话扩展,不会更改你的 Pi 配置。如果你希望自行管理 Pi 钩子,请禁用此选项。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "以標題前的單一彩色圖示表示工作區的代理、未讀與拉取請求狀態,而非各自佔一行。將游標移到圖示上可查看詳細資訊。" + "value": "啟用後,cmux 會包裝 pi 指令並載入內建工作階段擴充功能,不會變更你的 Pi 設定。若偏好自行管理 Pi 掛鉤,請停用此選項。" } - } - } - }, - "settings.search.alias.setting.app.compact-agent-status": { - "extractionState": "manual", - "localizations": { - "ar": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex مختصر وكيل حالة أيقونة سطر العنوان" + "value": "Kada je omogućeno, cmux obavija naredbu pi i učitava ugrađeno proširenje sesije bez mijenjanja vaše Pi konfiguracije. Onemogućite ako želite sami upravljati Pi kukama." } }, - "de": { + "da": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex kompakt Agent Status Symbol Titelzeile" + "value": "Når funktionen er aktiveret, indpakker cmux pi-kommandoen og indlæser den medfølgende sessionsudvidelse uden at ændre din Pi-konfiguration. Deaktiver den, hvis du selv vil administrere Pi-hooks." } }, - "en": { + "it": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex" + "value": "Quando è abilitata, cmux avvolge il comando pi e carica l'estensione di sessione inclusa senza modificare la configurazione di Pi. Disabilitala se preferisci gestire autonomamente gli hook di Pi." } }, - "es": { + "km": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex compacto agente estado icono línea de título" + "value": "នៅពេលបើកដំណើរការ cmux នឹងរុំពាក្យបញ្ជា pi ហើយផ្ទុកផ្នែកបន្ថែមសម័យដែលភ្ជាប់មកជាមួយ ដោយមិនផ្លាស់ប្តូរការកំណត់ Pi របស់អ្នក។ បិទវា ប្រសិនបើអ្នកចង់គ្រប់គ្រង hooks របស់ Pi ដោយខ្លួនឯង។" } }, - "fr": { + "nb": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex compact agent statut icône ligne de titre" + "value": "Når dette er aktivert, pakker cmux inn pi-kommandoen og laster den medfølgende øktutvidelsen uten å endre Pi-konfigurasjonen din. Deaktiver hvis du vil administrere Pi-hookene selv." } }, - "ja": { + "pl": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex コンパクト エージェント 状態 アイコン タイトル行" + "value": "Po włączeniu cmux opakuje polecenie pi i załaduje dołączone rozszerzenie sesji bez zmieniania konfiguracji Pi. Wyłącz tę opcję, jeśli wolisz samodzielnie zarządzać hookami Pi." } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex 간결 에이전트 상태 아이콘 제목 줄" + "value": "Quando ativado, o cmux envolve o comando pi e carrega a extensão de sessão incluída sem alterar sua configuração do Pi. Desative se preferir gerenciar os hooks do Pi por conta própria." } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex 精简 代理 状态 图标 标题行" + "value": "При включении cmux оборачивает команду pi и загружает встроенное расширение сеанса, не изменяя конфигурацию Pi. Отключите этот параметр, если предпочитаете управлять хуками Pi самостоятельно." } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex 精簡 代理 狀態 圖示 標題列" + "value": "เมื่อเปิดใช้งาน cmux จะครอบคำสั่ง pi และโหลดส่วนขยายเซสชันที่มาพร้อมกับโปรแกรมโดยไม่เปลี่ยนการตั้งค่า Pi ของคุณ ปิดใช้งานหากต้องการจัดการ hooks ของ Pi เอง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Etkinleştirildiğinde cmux, Pi yapılandırmanızı değiştirmeden pi komutunu sarar ve birlikte gelen oturum uzantısını yükler. Pi kancalarını kendiniz yönetmeyi tercih ediyorsanız devre dışı bırakın." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Якщо ввімкнено, cmux обгортає команду pi й завантажує вбудоване розширення сеансу, не змінюючи конфігурацію Pi. Вимкніть цей параметр, якщо хочете самостійно керувати хуками Pi." } } } }, - "sidebar.agentStatus.glyph.tooltip": { + "settings.automation.pi.subtitleOff": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Pi runs without cmux integration." } }, "ar": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "يعمل Pi بدون تكامل cmux." } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Pi läuft ohne cmux-Integration." } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Pi se ejecuta sin integración con cmux." } }, "fr": { "stringUnit": { "state": "translated", - "value": "%1$@ : %2$@" + "value": "Pi s’exécute sans intégration cmux." } }, "ja": { "stringUnit": { "state": "translated", - "value": "%1$@:%2$@" + "value": "Piはcmux連携なしで実行されます。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "Pi가 cmux 연동 없이 실행됩니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@:%2$@" + "value": "Pi 在没有 cmux 集成的情况下运行。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@:%2$@" + "value": "Pi 會在不使用 cmux 整合的情況下執行。" } - } - } - }, - "sidebar.compactStatus.profile": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "Profile" + "value": "Pi radi bez cmux integracije." } }, - "ar": { + "da": { "stringUnit": { "state": "translated", - "value": "الملف الشخصي" + "value": "Pi kører uden cmux-integration." } }, - "de": { + "it": { "stringUnit": { "state": "translated", - "value": "Profil" + "value": "Pi viene eseguito senza l'integrazione con cmux." } }, - "es": { + "km": { "stringUnit": { "state": "translated", - "value": "Perfil" + "value": "Pi ដំណើរការដោយគ្មានការរួមបញ្ចូល cmux។" } }, - "fr": { + "nb": { "stringUnit": { "state": "translated", - "value": "Profil" + "value": "Pi kjører uten cmux-integrasjon." } }, - "ja": { + "pl": { "stringUnit": { "state": "translated", - "value": "プロファイル" + "value": "Pi działa bez integracji z cmux." } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "프로필" + "value": "O Pi é executado sem a integração com o cmux." } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "配置文件" + "value": "Pi работает без интеграции с cmux." } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "設定檔" + "value": "Pi ทำงานโดยไม่มีการผสานรวมกับ cmux" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Pi, cmux entegrasyonu olmadan çalışır." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Pi працює без інтеграції з cmux." } } } }, - "terminal.clipboardReadConfirmation.deny": { + "settings.automation.pi.subtitleOn": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Deny" + "value": "Sidebar shows Pi session status and notifications." } }, "ar": { "stringUnit": { "state": "translated", - "value": "رفض" + "value": "يعرض الشريط الجانبي حالة جلسة Pi والإشعارات." } }, "de": { "stringUnit": { "state": "translated", - "value": "Ablehnen" + "value": "Die Seitenleiste zeigt den Pi-Sitzungsstatus und Benachrichtigungen an." } }, "es": { "stringUnit": { "state": "translated", - "value": "Denegar" + "value": "La barra lateral muestra el estado de la sesión de Pi y las notificaciones." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Refuser" + "value": "La barre latérale affiche le statut de la session Pi et les notifications." } }, "ja": { "stringUnit": { "state": "translated", - "value": "拒否" + "value": "サイドバーにPiセッションのステータスと通知が表示されます。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "거부" + "value": "사이드바에 Pi 세션 상태와 알림이 표시됩니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "拒绝" + "value": "侧边栏显示 Pi 会话状态和通知。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "拒絕" + "value": "側邊欄顯示 Pi 工作階段狀態和通知。" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Odbij" + "value": "Bočna traka prikazuje status Pi sesije i obavijesti." } }, "da": { "stringUnit": { "state": "translated", - "value": "Afvis" + "value": "Sidepanelet viser Pi-sessionens status og notifikationer." } }, "it": { "stringUnit": { "state": "translated", - "value": "Nega" + "value": "La barra laterale mostra lo stato della sessione Pi e le notifiche." } }, "km": { "stringUnit": { "state": "translated", - "value": "បដិសេធ" + "value": "របារចំហៀងបង្ហាញស្ថានភាពសម័យ Pi និងការជូនដំណឹង។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Avslå" + "value": "Sidepanelet viser status og varsler for Pi-økten." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Odmów" + "value": "Pasek boczny pokazuje stan sesji Pi i powiadomienia." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Negar" + "value": "A barra lateral mostra o status da sessão do Pi e as notificações." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Запретить" + "value": "На боковой панели отображаются состояние сеанса Pi и уведомления." } }, "th": { "stringUnit": { "state": "translated", - "value": "ปฏิเสธ" + "value": "แถบด้านข้างจะแสดงสถานะเซสชัน Pi และการแจ้งเตือน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Reddet" + "value": "Kenar çubuğu, Pi oturumunun durumunu ve bildirimleri gösterir." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Заборонити" + "value": "Бічна панель показує стан сеансу Pi та сповіщення." } } } }, - "terminal.clipboardReadConfirmation.message": { + "settings.search.alias.setting.automation.pi": { "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": "automation.piIntegration pi hooks agent integration status notifications" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يريد برنامج في هذه الطرفية قراءة الحافظة. اسمح بذلك فقط إذا كنت تثق بالبرنامج." + "value": "automation.piIntegration تكامل Pi خطافات وكيل حالة إشعارات" } }, - "de": { + "bs": { "stringUnit": { "state": "translated", - "value": "Ein Programm in diesem Terminal möchte deine Zwischenablage lesen. Erlaube es nur, wenn du dem Programm vertraust." + "value": "automation.piIntegration Pi kukice integracija agenta status obavijesti" } }, - "es": { + "da": { "stringUnit": { "state": "translated", - "value": "Un programa de esta terminal quiere leer tu portapapeles. Permítelo solo si confías en el programa." + "value": "automation.piIntegration Pi-hooks agentintegration status notifikationer" } }, - "fr": { + "de": { "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": "automation.piIntegration Pi Hooks Agent Integration Status Benachrichtigungen" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "このターミナルのプログラムがクリップボードを読み取ろうとしています。信頼できるプログラムの場合にのみ許可してください。" + "value": "automation.piIntegration Pi hooks integración de agente estado notificaciones" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "이 터미널의 프로그램이 클립보드를 읽으려고 합니다. 프로그램을 신뢰할 수 있을 때만 허용하세요." + "value": "automation.piIntegration Pi hooks intégration agent état notifications" } }, - "zh-Hans": { + "it": { "stringUnit": { "state": "translated", - "value": "此终端中的程序想要读取你的剪贴板。只有在你信任该程序时才允许。" + "value": "automation.piIntegration hook Pi integrazione agente stato notifiche" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "此終端機中的程式想要讀取你的剪貼簿。只有在你信任該程式時才允許。" + "value": "automation.piIntegration Pi フック エージェント連携 ステータス 通知" } }, - "bs": { + "km": { "stringUnit": { "state": "translated", - "value": "Program u ovom terminalu želi čitati vaš međuspremnik. Dozvolite samo ako vjerujete programu." + "value": "automation.piIntegration hooks Pi ការរួមបញ្ចូលភ្នាក់ងារ ស្ថានភាព ការជូនដំណឹង" } }, - "da": { + "ko": { "stringUnit": { "state": "translated", - "value": "Et program i denne terminal vil læse din udklipsholder. Tillad det kun, hvis du har tillid til programmet." + "value": "automation.piIntegration Pi 훅 에이전트 연동 상태 알림" } }, - "it": { + "nb": { "stringUnit": { "state": "translated", - "value": "Un programma in questo terminale vuole leggere gli appunti. Consentilo solo se ti fidi del programma." + "value": "automation.piIntegration Pi-hook agentintegrasjon status varsler" } }, - "km": { + "pl": { "stringUnit": { "state": "translated", - "value": "កម្មវិធីក្នុងស្ថានីយនេះចង់អានក្ដារតម្បៀតខ្ទាស់របស់អ្នក។ អនុញ្ញាតតែបើអ្នកទុកចិត្តកម្មវិធីនេះ។" + "value": "automation.piIntegration hooki Pi integracja agenta stan powiadomienia" } }, - "nb": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Et program i denne terminalen vil lese utklippstavlen din. Tillat det bare hvis du stoler på programmet." + "value": "automation.piIntegration hooks do Pi integração do agente status notificações" } }, - "pl": { + "ru": { "stringUnit": { "state": "translated", - "value": "Program w tym terminalu chce odczytać schowek. Zezwól tylko, jeśli ufasz temu programowi." + "value": "automation.piIntegration хуки Pi интеграция агента состояние уведомления" } }, - "pt-BR": { + "th": { "stringUnit": { "state": "translated", - "value": "Um programa neste terminal quer ler sua área de transferência. Permita somente se você confiar no programa." + "value": "automation.piIntegration hooks ของ Pi การผสานรวมเอเจนต์ สถานะ การแจ้งเตือน" } }, - "ru": { + "tr": { "stringUnit": { "state": "translated", - "value": "Программа в этом терминале хочет прочитать буфер обмена. Разрешайте только программам, которым доверяете." + "value": "automation.piIntegration Pi kancaları aracı entegrasyonu durum bildirimler" } }, - "th": { + "uk": { "stringUnit": { "state": "translated", - "value": "โปรแกรมในเทอร์มินัลนี้ต้องการอ่านคลิปบอร์ดของคุณ อนุญาตเฉพาะเมื่อคุณเชื่อถือโปรแกรมนี้" + "value": "automation.piIntegration хуки Pi інтеграція агента стан сповіщення" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Bu terminaldeki bir program panonuzu okumak istiyor. Yalnızca programa güveniyorsanız izin verin." + "value": "automation.piIntegration Pi 钩子 代理集成 状态 通知" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Програма в цьому терміналі хоче прочитати ваш буфер обміну. Дозволяйте, лише якщо довіряєте програмі." + "value": "automation.piIntegration Pi 掛鉤 代理程式整合 狀態 通知" } } } }, - "terminal.clipboardReadConfirmation.title": { + "cli.hooks.setup.wrapperNote": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Allow Clipboard Access?" + "value": " (Claude Code and Pi hooks are injected automatically via their cmux wrappers)" } }, "ar": { "stringUnit": { "state": "translated", - "value": "السماح بالوصول إلى الحافظة؟" + "value": " (يتم حقن خطافات Claude Code وPi تلقائيًا عبر أغلفة cmux)" } }, - "de": { + "bs": { "stringUnit": { "state": "translated", - "value": "Zugriff auf die Zwischenablage erlauben?" + "value": " (Claude Code i Pi kukice se automatski ubacuju putem cmux omotača)" } }, - "es": { + "da": { "stringUnit": { "state": "translated", - "value": "¿Permitir el acceso al portapapeles?" + "value": " (Claude Code- og Pi-hooks indsættes automatisk via deres cmux-wrappere)" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "Autoriser l’accès au presse-papiers ?" + "value": " (Claude-Code- und Pi-Hooks werden automatisch über ihre cmux-Wrapper eingefügt)" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "クリップボードへのアクセスを許可しますか?" + "value": " (Los hooks de Claude Code y Pi se inyectan automáticamente mediante sus wrappers de cmux)" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "클립보드 접근을 허용하시겠습니까?" + "value": " (Les hooks de Claude Code et Pi sont injectés automatiquement par leurs wrappers cmux)" } }, - "zh-Hans": { + "it": { "stringUnit": { "state": "translated", - "value": "允许访问剪贴板?" + "value": " (Gli hook di Claude Code e Pi vengono iniettati automaticamente tramite i rispettivi wrapper cmux)" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "允許存取剪貼簿?" + "value": " (Claude Code と Pi のフックは cmux ラッパーによって自動的に注入されます)" } }, - "bs": { + "km": { "stringUnit": { "state": "translated", - "value": "Dozvoliti pristup međuspremniku?" + "value": " (hooks របស់ Claude Code និង Pi ត្រូវបានបញ្ចូលដោយស្វ័យប្រវត្តិតាមរយៈ wrapper របស់ cmux)" } }, - "da": { + "ko": { "stringUnit": { "state": "translated", - "value": "Tillad adgang til udklipsholderen?" + "value": " (Claude Code 및 Pi 훅은 cmux 래퍼를 통해 자동으로 주입됩니다)" } }, - "it": { + "nb": { "stringUnit": { "state": "translated", - "value": "Consentire l'accesso agli appunti?" + "value": " (Claude Code- og Pi-hooker settes inn automatisk via cmux-innpakningene)" } }, - "km": { + "pl": { "stringUnit": { "state": "translated", - "value": "អនុញ្ញាតឱ្យចូលប្រើក្ដារតម្បៀតខ្ទាស់?" + "value": " (Hooki Claude Code i Pi są automatycznie wstrzykiwane przez ich opakowania cmux)" } }, - "nb": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Tillat tilgang til utklippstavlen?" + "value": " (Os hooks do Claude Code e do Pi são injetados automaticamente pelos respectivos wrappers do cmux)" } }, - "pl": { + "ru": { "stringUnit": { "state": "translated", - "value": "Zezwolić na dostęp do schowka?" + "value": " (Хуки Claude Code и Pi автоматически внедряются их оболочками cmux)" } }, - "pt-BR": { + "th": { "stringUnit": { "state": "translated", - "value": "Permitir acesso à área de transferência?" + "value": " (hooks ของ Claude Code และ Pi จะถูกฉีดโดยอัตโนมัติผ่าน wrapper ของ cmux)" } }, - "ru": { + "tr": { "stringUnit": { "state": "translated", - "value": "Разрешить доступ к буферу обмена?" + "value": " (Claude Code ve Pi kancaları, cmux sarmalayıcıları aracılığıyla otomatik olarak eklenir)" } }, - "th": { + "uk": { "stringUnit": { "state": "translated", - "value": "อนุญาตให้เข้าถึงคลิปบอร์ดหรือไม่?" + "value": " (Хуки Claude Code і Pi автоматично додаються їхніми обгортками cmux)" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Panoya erişime izin verilsin mi?" + "value": " (Claude Code 和 Pi 的钩子会通过各自的 cmux 包装器自动注入)" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Дозволити доступ до буфера обміну?" + "value": " (Claude Code 和 Pi 的掛鉤會透過各自的 cmux 包裝器自動注入)" } } } }, - "notification.remoteRelay.hostFallback": { + "devices.link.error.notDiscoverable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Remote host" + "value": "This Mac isn’t discoverable. On that Mac, turn on “Make this Mac discoverable” in Settings › Devices." } }, "ar": { "stringUnit": { "state": "translated", - "value": "مضيف بعيد" + "value": "جهاز Mac هذا غير قابل للاكتشاف. على ذلك الجهاز، فعّل «جعل جهاز Mac هذا قابلاً للاكتشاف» في الإعدادات › الأجهزة." } }, "de": { "stringUnit": { "state": "translated", - "value": "Remote-Host" + "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": "Host remoto" + "value": "Este Mac no es detectable. En ese Mac, activa «Hacer que este Mac sea detectable» en Ajustes › Dispositivos." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Hôte distant" + "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": "リモートホスト" + "value": "このMacは検出可能になっていません。そのMacの「設定 › デバイス」で「このMacを検出可能にする」をオンにしてください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "원격 호스트" + "value": "이 Mac을 검색할 수 없습니다. 해당 Mac의 설정 › 기기에서 “이 Mac을 검색할 수 있도록 설정”을 켜세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "远程主机" + "value": "这台 Mac 无法被发现。请在该 Mac 的“设置 › 设备”中开启“允许发现此 Mac”。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "遠端主機" + "value": "這部 Mac 無法被探索。請在該 Mac 的「設定 › 裝置」中開啟「允許探索此 Mac」。" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Udaljeni host" + "value": "Ovaj Mac nije vidljiv. Na tom Macu uključite „Učini ovaj Mac vidljivim” u Postavke › Uređaji." } }, "da": { "stringUnit": { "state": "translated", - "value": "Fjernvært" + "value": "Denne Mac kan ikke findes. Slå “Gør denne Mac synlig” til på den Mac under Indstillinger › Enheder." } }, "it": { "stringUnit": { "state": "translated", - "value": "Host remoto" + "value": "Questo Mac non è individuabile. Su quel Mac, attiva “Rendi individuabile questo Mac” in Impostazioni › Dispositivi." } }, "km": { "stringUnit": { "state": "translated", - "value": "ម៉ាស៊ីនពីចម្ងាយ" + "value": "Mac នេះមិនអាចរកឃើញបានទេ។ នៅលើ Mac នោះ សូមបើក “ធ្វើឲ្យ Mac នេះអាចរកឃើញ” ក្នុងការកំណត់ › ឧបករណ៍។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Ekstern vert" + "value": "Denne Macen er ikke synlig. Slå på «Gjør denne Macen synlig» på den Macen under Innstillinger › Enheter." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Zdalny host" + "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": "Host remoto" + "value": "Este Mac não está detectável. Nesse Mac, ative “Tornar este Mac detectável” em Ajustes › Dispositivos." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Удалённый хост" + "value": "Этот Mac недоступен для обнаружения. На том Mac включите «Сделать этот Mac доступным для обнаружения» в разделе Настройки › Устройства." } }, "th": { "stringUnit": { "state": "translated", - "value": "โฮสต์ระยะไกล" + "value": "Mac เครื่องนี้ไม่สามารถค้นพบได้ บน Mac เครื่องนั้น ให้เปิด “ทำให้ Mac เครื่องนี้ค้นพบได้” ในการตั้งค่า › อุปกรณ์" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Uzak ana bilgisayar" + "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": "Віддалений хост" + "value": "Цей Mac недоступний для виявлення. На тому Mac увімкніть «Зробити цей Mac доступним для виявлення» у розділі Налаштування › Пристрої." } } } }, - "notification.remoteRelay.title": { + "socket.terminal.notRunning.awaitingRestore": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux is still restoring the terminal after it reopened and is checking which agent session to resume. Retry in a few seconds." } }, "ar": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "لا يزال cmux يستعيد الطرفية بعد إعادة فتحه ويتحقق من جلسة الوكيل التي يجب استئنافها. أعد المحاولة بعد بضع ثوانٍ." } }, "de": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux stellt das Terminal nach dem erneuten Öffnen noch wieder her und prüft, welche Agent-Sitzung fortgesetzt werden soll. Versuche es in ein paar Sekunden erneut." } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux todavía está restaurando la terminal después de volver a abrirse y comprueba qué sesión de agente reanudar. Vuelve a intentarlo en unos segundos." } }, "fr": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux restaure encore le terminal après sa réouverture et vérifie quelle session d’agent reprendre. Réessayez dans quelques secondes." } }, "ja": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux は再起動後にターミナルを復元中で、再開するエージェントセッションを確認しています。数秒後に再試行してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux가 다시 열린 후 터미널을 아직 복원하는 중이며 어떤 에이전트 세션을 재개할지 확인하고 있습니다. 몇 초 후에 다시 시도하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux 重新打开后仍在恢复此终端,并确认要恢复哪个代理会话。请几秒后重试。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux 重新開啟後仍在還原此終端機,並確認要恢復哪個代理工作階段。請幾秒後再試。" } }, "bs": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux još vraća terminal nakon ponovnog otvaranja i provjerava koju sesiju agenta treba nastaviti. Pokušajte ponovo za nekoliko sekundi." } }, "da": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux gendanner stadig terminalen efter genåbning og tjekker, hvilken agentsession der skal genoptages. Prøv igen om et par sekunder." } }, "it": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux sta ancora ripristinando il terminale dopo la riapertura e verifica quale sessione dell’agente riprendere. Riprova tra qualche secondo." } }, "km": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux នៅតែកំពុងស្ដារទែមីណាល់ឡើងវិញ បន្ទាប់ពីបើកម្ដងទៀត ហើយកំពុងពិនិត្យមើលថាត្រូវបន្តវគ្គភ្នាក់ងារមួយណា។ សូមព្យាយាមម្ដងទៀតក្នុងរយៈពេលពីរបីវិនាទី។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux gjenoppretter fortsatt terminalen etter at den ble åpnet på nytt, og sjekker hvilken agentøkt som skal gjenopptas. Prøv igjen om noen sekunder." } }, "pl": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux wciąż przywraca terminal po ponownym otwarciu i sprawdza, którą sesję agenta wznowić. Spróbuj ponownie za kilka sekund." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "O cmux ainda está restaurando o terminal após ser reaberto e verificando qual sessão do agente retomar. Tente novamente em alguns segundos." } }, "ru": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux ещё восстанавливает терминал после повторного открытия и проверяет, какой сеанс агента возобновить. Повторите попытку через несколько секунд." } }, "th": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux ยังคงกู้คืนเทอร์มินัลหลังจากเปิดใหม่ และกำลังตรวจสอบว่าจะทำเซสชันเอเจนต์ใดต่อ ลองอีกครั้งในอีกไม่กี่วินาที" } }, "tr": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux yeniden açıldıktan sonra terminali hâlâ geri yüklüyor ve hangi ajan oturumunun sürdürüleceğini denetliyor. Birkaç saniye sonra yeniden deneyin." } }, "uk": { "stringUnit": { "state": "translated", - "value": "%1$@: %2$@" + "value": "cmux ще відновлює термінал після повторного відкриття та перевіряє, який сеанс агента відновити. Спробуйте ще раз за кілька секунд." } } } }, - "socket.mainActorHop.timeout.mayHaveRun": { + "socket.terminal.notRunning.closing": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux did not respond within 10 seconds after the command started, so its result is unknown. Check the effect before retrying." + "value": "The terminal is closing and cannot be read." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "コマンドの開始後 10 秒以内に cmux が応答しなかったため、結果は不明です。再試行する前に効果を確認してください。" + "value": "الطرفية قيد الإغلاق ولا يمكن قراءتها." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "命令开始后 10 秒内 cmux 没有响应,因此结果未知。请先确认效果再重试。" + "value": "Das Terminal wird geschlossen und kann nicht gelesen werden." } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux hat innerhalb von 10 Sekunden nach dem Start des Befehls nicht geantwortet, das Ergebnis ist daher unbekannt. Prüfe die Auswirkung, bevor du es erneut versuchst." + "value": "La terminal se está cerrando y no se puede leer." } }, "fr": { "stringUnit": { "state": "translated", - "value": "cmux n’a pas répondu dans les 10 secondes suivant le démarrage de la commande, son résultat est donc inconnu. Vérifiez l’effet avant de réessayer." + "value": "Le terminal est en cours de fermeture et ne peut pas être lu." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "لم يستجب cmux خلال 10 ثوانٍ بعد بدء الأمر، لذا فإن نتيجته غير معروفة. تحقق من الأثر قبل إعادة المحاولة." + "value": "ターミナルは閉じられているため読み取れません。" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux no respondió en los 10 segundos posteriores al inicio del comando, por lo que su resultado es desconocido. Comprueba el efecto antes de reintentar." + "value": "터미널이 닫히는 중이라 읽을 수 없습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此终端正在关闭,无法读取。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "命令開始後 10 秒內 cmux 沒有回應,因此結果未知。請先確認效果再重試。" + "value": "此終端機正在關閉,無法讀取。" } }, - "ko": { + "bs": { "stringUnit": { "state": "translated", - "value": "명령이 시작된 후 10초 안에 cmux가 응답하지 않아 결과를 알 수 없습니다. 다시 시도하기 전에 효과를 확인하세요." + "value": "Terminal se zatvara i ne može se pročitati." } - } - } - }, - "socket.mainActorHop.timeout.notRun": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "cmux did not respond within 10 seconds, so the command was not run. Retry in a moment." + "value": "Terminalen lukkes og kan ikke læses." } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "cmux が 10 秒以内に応答しなかったため、コマンドは実行されませんでした。しばらくしてから再試行してください。" + "value": "Il terminale si sta chiudendo e non può essere letto." } }, - "zh-Hans": { + "km": { "stringUnit": { "state": "translated", - "value": "cmux 在 10 秒内没有响应,因此未运行该命令。请稍后重试。" + "value": "ទែមីណាល់កំពុងបិទ ហើយមិនអាចអានបានទេ។" } }, - "de": { + "nb": { "stringUnit": { "state": "translated", - "value": "cmux hat innerhalb von 10 Sekunden nicht geantwortet, der Befehl wurde daher nicht ausgeführt. Versuche es gleich noch einmal." + "value": "Terminalen lukkes og kan ikke leses." } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "cmux n’a pas répondu dans les 10 secondes, la commande n’a donc pas été exécutée. Réessayez dans un instant." + "value": "Terminal jest zamykany i nie można go odczytać." } }, - "ar": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "لم يستجب cmux خلال 10 ثوانٍ، لذا لم يتم تنفيذ الأمر. أعد المحاولة بعد لحظات." + "value": "O terminal está sendo fechado e não pode ser lido." } }, - "es": { + "ru": { "stringUnit": { "state": "translated", - "value": "cmux no respondió en 10 segundos, por lo que el comando no se ejecutó. Reintenta en un momento." + "value": "Терминал закрывается, и его нельзя прочитать." } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "cmux 在 10 秒內沒有回應,因此未執行該命令。請稍後重試。" + "value": "เทอร์มินัลกำลังปิดและไม่สามารถอ่านได้" } }, - "ko": { + "tr": { "stringUnit": { "state": "translated", - "value": "cmux가 10초 안에 응답하지 않아 명령이 실행되지 않았습니다. 잠시 후 다시 시도하세요." + "value": "Terminal kapanıyor ve okunamıyor." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Термінал закривається, і його не можна прочитати." } } } }, - "socket.overloaded.message": { + "socket.terminal.notRunning.hibernated": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux is handling too many control-socket requests right now, so this one was not run. Retry in a moment." + "value": "The terminal is hibernated: cmux suspended its idle agent to save memory. Wake it by showing it (this brings its window to the front), then retry: %@" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux は現在、制御ソケットの要求を処理しきれないため、この要求は実行されませんでした。しばらくしてから再試行してください。" + "value": "الطرفية في وضع السبات: أوقف cmux وكيلها الخامل مؤقتًا لتوفير الذاكرة. أيقظها بعرضها (يؤدي ذلك إلى إحضار نافذتها إلى المقدمة)، ثم أعد المحاولة: %@" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "cmux 当前正在处理过多的控制套接字请求,因此未运行此请求。请稍后重试。" + "value": "Das Terminal ruht: cmux hat seinen inaktiven Agenten angehalten, um Speicher zu sparen. Wecke es, indem du es anzeigst (dadurch kommt sein Fenster in den Vordergrund), und versuche es dann erneut: %@" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux verarbeitet gerade zu viele Control-Socket-Anfragen, diese wurde daher nicht ausgeführt. Versuche es gleich noch einmal." + "value": "La terminal está hibernada: cmux suspendió su agente inactivo para ahorrar memoria. Despiértala mostrándola (esto trae su ventana al frente) y vuelve a intentarlo: %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "cmux traite actuellement trop de requêtes du socket de contrôle, celle-ci n’a donc pas été exécutée. Réessayez dans un instant." + "value": "Le terminal est en veille : cmux a suspendu son agent inactif pour économiser de la mémoire. Réveillez-le en l’affichant (sa fenêtre passe au premier plan), puis réessayez : %@" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "يعالج cmux حاليًا عددًا كبيرًا جدًا من طلبات مقبس التحكم، لذا لم يتم تنفيذ هذا الطلب. أعد المحاولة بعد لحظات." + "value": "ターミナルは休止中です。cmux がメモリ節約のためアイドル状態のエージェントを停止しました。表示すると再開します(ウィンドウが前面に出ます)。その後、再試行してください: %@" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux está gestionando demasiadas solicitudes del socket de control ahora mismo, por lo que esta no se ejecutó. Reintenta en un momento." + "value": "터미널이 최대 절전 상태입니다. cmux가 메모리를 절약하려고 유휴 에이전트를 일시 중단했습니다. 터미널을 표시해 깨운 후(창이 앞으로 옵니다) 다시 시도하세요: %@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此终端已休眠:cmux 暂停了其空闲的代理以节省内存。显示它即可唤醒(这会将其窗口带到最前面),然后重试:%@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux 目前正在處理過多的控制 socket 請求,因此未執行此請求。請稍後重試。" + "value": "此終端機已休眠:cmux 暫停了其閒置的代理以節省記憶體。顯示它即可喚醒(這會將其視窗帶到最前面),然後再試一次:%@" } }, - "ko": { + "bs": { "stringUnit": { "state": "translated", - "value": "cmux가 지금 너무 많은 제어 소켓 요청을 처리하고 있어 이 요청은 실행되지 않았습니다. 잠시 후 다시 시도하세요." + "value": "Terminal je u hibernaciji: cmux je pauzirao njegovog neaktivnog agenta radi uštede memorije. Probudite ga tako što ćete ga prikazati (njegov prozor prelazi u prvi plan), pa pokušajte ponovo: %@" } - } - } - }, - "socket.request.cancelled": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Request was cancelled" + "value": "Terminalen er i dvale: cmux har sat dens inaktive agent på pause for at spare hukommelse. Væk den ved at vise den (dens vindue kommer forrest), og prøv igen: %@" } }, - "ja": { + "it": { "stringUnit": { "state": "translated", - "value": "リクエストはキャンセルされました" + "value": "Il terminale è ibernato: cmux ha sospeso il suo agente inattivo per risparmiare memoria. Riattivalo mostrandolo (la sua finestra passa in primo piano), quindi riprova: %@" } }, - "zh-Hans": { + "km": { "stringUnit": { "state": "translated", - "value": "请求已取消" + "value": "ទែមីណាល់កំពុងសម្រាក៖ cmux បានផ្អាកភ្នាក់ងារដែលទំនេររបស់វា ដើម្បីសន្សំអង្គចងចាំ។ ដាស់វាដោយបង្ហាញវា (បង្អួចរបស់វានឹងមកខាងមុខ) រួចព្យាយាមម្ដងទៀត៖ %@" } }, - "de": { + "nb": { "stringUnit": { "state": "translated", - "value": "Die Anfrage wurde abgebrochen" + "value": "Terminalen er i dvale: cmux har satt den inaktive agenten på pause for å spare minne. Vekk den ved å vise den (vinduet kommer i forgrunnen), og prøv igjen: %@" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "La requête a été annulée" + "value": "Terminal jest uśpiony: cmux wstrzymał jego bezczynnego agenta, aby oszczędzić pamięć. Wybudź go, wyświetlając go (jego okno przejdzie na pierwszy plan), a potem spróbuj ponownie: %@" } }, - "ar": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "تم إلغاء الطلب" + "value": "O terminal está hibernado: o cmux suspendeu o agente ocioso para economizar memória. Desperte-o exibindo-o (a janela vem para a frente) e tente novamente: %@" } }, - "es": { + "ru": { "stringUnit": { "state": "translated", - "value": "Se canceló la solicitud" + "value": "Терминал в спящем режиме: cmux приостановил неактивного агента, чтобы сэкономить память. Разбудите его, показав терминал (его окно выйдет на передний план), и повторите попытку: %@" } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "請求已取消" + "value": "เทอร์มินัลอยู่ในโหมดพัก: cmux หยุดเอเจนต์ที่ไม่ได้ใช้งานชั่วคราวเพื่อประหยัดหน่วยความจำ ปลุกโดยแสดงเทอร์มินัล (หน้าต่างจะมาอยู่ด้านหน้า) แล้วลองอีกครั้ง: %@" } }, - "ko": { + "tr": { "stringUnit": { "state": "translated", - "value": "요청이 취소되었습니다" + "value": "Terminal hazırda bekliyor: cmux bellek tasarrufu için boştaki ajanını askıya aldı. Onu göstererek uyandırın (penceresi öne gelir), ardından yeniden deneyin: %@" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Термінал у режимі сну: cmux призупинив неактивного агента, щоб заощадити пам’ять. Розбудіть його, показавши термінал (його вікно перейде на передній план), і спробуйте ще раз: %@" } } } }, - "socket.request.failedBeforeResult": { + "socket.terminal.notRunning.hibernatedNoCommand": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Request failed before returning a result" + "value": "The terminal is hibernated: cmux suspended its idle agent to save memory. Show the terminal in cmux to wake it, then retry." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "結果を返す前にリクエストが失敗しました" + "value": "الطرفية في وضع السبات: أوقف cmux وكيلها الخامل مؤقتًا لتوفير الذاكرة. اعرض الطرفية في cmux لإيقاظها، ثم أعد المحاولة." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "请求在返回结果之前失败" + "value": "Das Terminal ruht: cmux hat seinen inaktiven Agenten angehalten, um Speicher zu sparen. Zeige das Terminal in cmux an, um es zu wecken, und versuche es dann erneut." } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "Die Anfrage ist fehlgeschlagen, bevor ein Ergebnis zurückgegeben wurde" + "value": "La terminal está hibernada: cmux suspendió su agente inactivo para ahorrar memoria. Muestra la terminal en cmux para despertarla y vuelve a intentarlo." } }, "fr": { "stringUnit": { "state": "translated", - "value": "La requête a échoué avant de renvoyer un résultat" + "value": "Le terminal est en veille : cmux a suspendu son agent inactif pour économiser de la mémoire. Affichez le terminal dans cmux pour le réveiller, puis réessayez." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "فشل الطلب قبل إرجاع نتيجة" + "value": "ターミナルは休止中です。cmux がメモリ節約のためアイドル状態のエージェントを停止しました。cmux でターミナルを表示して再開してから、再試行してください。" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "La solicitud falló antes de devolver un resultado" + "value": "터미널이 최대 절전 상태입니다. cmux가 메모리를 절약하려고 유휴 에이전트를 일시 중단했습니다. cmux에서 터미널을 표시해 깨운 후 다시 시도하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此终端已休眠:cmux 暂停了其空闲的代理以节省内存。请在 cmux 中显示此终端以唤醒它,然后重试。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請求在傳回結果之前失敗" + "value": "此終端機已休眠:cmux 暫停了其閒置的代理以節省記憶體。請在 cmux 中顯示此終端機以喚醒它,然後再試一次。" } }, - "ko": { + "bs": { "stringUnit": { "state": "translated", - "value": "결과를 반환하기 전에 요청이 실패했습니다" + "value": "Terminal je u hibernaciji: cmux je pauzirao njegovog neaktivnog agenta radi uštede memorije. Prikažite terminal u cmuxu da ga probudite, pa pokušajte ponovo." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Terminalen er i dvale: cmux har sat dens inaktive agent på pause for at spare hukommelse. Vis terminalen i cmux for at vække den, og prøv igen." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il terminale è ibernato: cmux ha sospeso il suo agente inattivo per risparmiare memoria. Mostra il terminale in cmux per riattivarlo, quindi riprova." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ទែមីណាល់កំពុងសម្រាក៖ cmux បានផ្អាកភ្នាក់ងារដែលទំនេររបស់វា ដើម្បីសន្សំអង្គចងចាំ។ បង្ហាញទែមីណាល់នៅក្នុង cmux ដើម្បីដាស់វា រួចព្យាយាមម្ដងទៀត។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Terminalen er i dvale: cmux har satt den inaktive agenten på pause for å spare minne. Vis terminalen i cmux for å vekke den, og prøv igjen." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Terminal jest uśpiony: cmux wstrzymał jego bezczynnego agenta, aby oszczędzić pamięć. Wyświetl terminal w cmux, aby go wybudzić, a potem spróbuj ponownie." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O terminal está hibernado: o cmux suspendeu o agente ocioso para economizar memória. Mostre o terminal no cmux para despertá-lo e tente novamente." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Терминал в спящем режиме: cmux приостановил неактивного агента, чтобы сэкономить память. Покажите терминал в cmux, чтобы разбудить его, и повторите попытку." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เทอร์มินัลอยู่ในโหมดพัก: cmux หยุดเอเจนต์ที่ไม่ได้ใช้งานชั่วคราวเพื่อประหยัดหน่วยความจำ แสดงเทอร์มินัลใน cmux เพื่อปลุก แล้วลองอีกครั้ง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Terminal hazırda bekliyor: cmux bellek tasarrufu için boştaki ajanını askıya aldı. Uyandırmak için terminali cmux'ta gösterin, ardından yeniden deneyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Термінал у режимі сну: cmux призупинив неактивного агента, щоб заощадити пам’ять. Покажіть термінал у cmux, щоб розбудити його, і спробуйте ще раз." } } } }, - "agent.codex.fork.notice.body": { + "socket.terminal.notRunning.starting": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux could not identify the new fork session. Start the fork again from the parent pane." + "value": "The terminal has not started yet. Retry in a few seconds." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux は新しいフォーク セッションを特定できませんでした。親ペインからもう一度フォークしてください。" + "value": "لم تبدأ الطرفية بعد. أعد المحاولة بعد بضع ثوانٍ." } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux konnte die neue Fork-Sitzung nicht erkennen. Starten Sie den Fork erneut im übergeordneten Pane." + "value": "Das Terminal wurde noch nicht gestartet. Versuche es in ein paar Sekunden erneut." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux n’a pas pu identifier la nouvelle session forkée. Relancez le fork depuis le volet parent." + "value": "La terminal aún no se ha iniciado. Vuelve a intentarlo en unos segundos." } }, - "ar": { + "fr": { "stringUnit": { "state": "translated", - "value": "تعذر على cmux تحديد جلسة التفريع الجديدة. ابدأ التفريع مرة أخرى من الجزء الأصلي." + "value": "Le terminal n’a pas encore démarré. Réessayez dans quelques secondes." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "cmux no pudo identificar la nueva sesión bifurcada. Inicia el fork de nuevo desde el panel principal." + "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": "此終端機尚未啟動。請幾秒後再試。" } }, "bs": { "stringUnit": { "state": "translated", - "value": "cmux nije mogao prepoznati novu sesiju račvanja. Ponovo pokrenite račvanje iz nadređenog okna." + "value": "Terminal se još nije pokrenuo. Pokušajte ponovo za nekoliko sekundi." } }, "da": { "stringUnit": { "state": "translated", - "value": "cmux kunne ikke identificere den nye forgreningssession. Start forgreningen igen fra forældreruden." + "value": "Terminalen er ikke startet endnu. Prøv igen om et par sekunder." } }, "it": { "stringUnit": { "state": "translated", - "value": "cmux non ha potuto identificare la nuova sessione fork. Avvia di nuovo il fork dal riquadro principale." + "value": "Il terminale non è ancora stato avviato. Riprova tra qualche secondo." } }, "km": { "stringUnit": { "state": "translated", - "value": "cmux មិនអាចកំណត់អត្តសញ្ញាណសម័យ fork ថ្មីបានទេ។ ចាប់ផ្ដើម fork ម្ដងទៀតពីផ្ទាំងមេ។" + "value": "ទែមីណាល់មិនទាន់បានចាប់ផ្តើមនៅឡើយទេ។ សូមព្យាយាមម្ដងទៀតក្នុងរយៈពេលពីរបីវិនាទី។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "cmux kunne ikke identifisere den nye forgreningsøkten. Start forgreningen på nytt fra den overordnede ruten." + "value": "Terminalen har ikke startet ennå. Prøv igjen om noen sekunder." } }, "pl": { "stringUnit": { "state": "translated", - "value": "cmux nie mógł zidentyfikować nowej sesji forka. Uruchom fork ponownie z panelu nadrzędnego." + "value": "Terminal jeszcze się nie uruchomił. Spróbuj ponownie za kilka sekund." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O cmux não conseguiu identificar a nova sessão bifurcada. Inicie o fork novamente no painel principal." + "value": "O terminal ainda não foi iniciado. Tente novamente em alguns segundos." } }, "ru": { "stringUnit": { "state": "translated", - "value": "cmux не удалось определить новый сеанс форка. Запустите форк снова из родительской панели." + "value": "Терминал ещё не запущен. Повторите попытку через несколько секунд." } }, "th": { "stringUnit": { "state": "translated", - "value": "cmux ไม่สามารถระบุเซสชันที่แยกใหม่ได้ เริ่มการแยกอีกครั้งจากบานหน้าต่างหลัก" + "value": "เทอร์มินัลยังไม่ได้เริ่มทำงาน ลองอีกครั้งในอีกไม่กี่วินาที" } }, "tr": { "stringUnit": { "state": "translated", - "value": "cmux yeni fork oturumunu belirleyemedi. Fork'u üst panelden yeniden başlatın." + "value": "Terminal henüz başlamadı. Birkaç saniye sonra yeniden deneyin." } }, "uk": { "stringUnit": { "state": "translated", - "value": "cmux не вдалося визначити новий сеанс форка. Запустіть форк ще раз із батьківської панелі." + "value": "Термінал ще не запущено. Спробуйте ще раз за кілька секунд." } } } }, - "agent.codex.fork.notice.parentClearFailed.body": { + "cli.auth.status.teamNotLoaded": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux could not safely bind the new fork session. Retry the fork from the parent pane." + "value": " team: not loaded" } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux は新しいフォーク セッションを安全にバインドできませんでした。親ペインからもう一度フォークしてください。" + "value": " チーム: 未読み込み" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": " 团队: 未加载" } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux konnte die neue Fork-Sitzung nicht sicher binden. Starten Sie den Fork erneut im übergeordneten Pane." + "value": " Team: nicht geladen" } }, "fr": { "stringUnit": { "state": "translated", - "value": "cmux n’a pas pu lier la nouvelle session forkée en toute sécurité. Relancez le fork depuis le volet parent." + "value": " équipe: non chargée" } }, "ar": { "stringUnit": { "state": "translated", - "value": "تعذر على cmux ربط جلسة التفريع الجديدة بأمان. أعد بدء التفريع من الجزء الأصلي." + "value": " الفريق: لم يتم تحميله" } }, "es": { "stringUnit": { "state": "translated", - "value": "cmux no pudo vincular de forma segura la nueva sesión bifurcada. Inicia el fork de nuevo desde el panel principal." + "value": " equipo: no cargado" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux 無法安全繫結新的分支工作階段。請從父窗格重新開始分支。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux 无法安全绑定新的分支会话。请从父窗格重新开始分支。" + "value": " 團隊: 未載入" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux가 새 포크 세션을 안전하게 연결하지 못했습니다. 부모 패널에서 포크를 다시 시작하세요." + "value": " 팀: 로드되지 않음" } }, "bs": { "stringUnit": { "state": "translated", - "value": "cmux nije mogao sigurno povezati novu sesiju račvanja. Ponovo pokrenite račvanje iz nadređenog okna." + "value": " tim: nije učitan" } }, "da": { "stringUnit": { "state": "translated", - "value": "cmux kunne ikke binde den nye forgreningssession sikkert. Start forgreningen igen fra forældreruden." + "value": " team: ikke indlæst" } }, "it": { "stringUnit": { "state": "translated", - "value": "cmux non ha potuto associare in sicurezza la nuova sessione fork. Avvia di nuovo il fork dal riquadro principale." + "value": " team: non caricato" } }, "km": { "stringUnit": { "state": "translated", - "value": "cmux មិនអាចភ្ជាប់សម័យ fork ថ្មីដោយសុវត្ថិភាពបានទេ។ ចាប់ផ្ដើម fork ម្ដងទៀតពីផ្ទាំងមេ។" + "value": " ក្រុម៖ មិនទាន់បានផ្ទុក" } }, "nb": { "stringUnit": { "state": "translated", - "value": "cmux kunne ikke binde den nye forgreningsøkten på en trygg måte. Start forgreningen på nytt fra den overordnede ruten." + "value": " team: ikke lastet" } }, "pl": { "stringUnit": { "state": "translated", - "value": "cmux nie mógł bezpiecznie powiązać nowej sesji forka. Uruchom fork ponownie z panelu nadrzędnego." + "value": " zespół: nie załadowano" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "O cmux não conseguiu vincular com segurança a nova sessão bifurcada. Inicie o fork novamente no painel principal." + "value": " equipe: não carregada" } }, "ru": { "stringUnit": { "state": "translated", - "value": "cmux не удалось безопасно привязать новый сеанс форка. Запустите форк снова из родительской панели." + "value": " команда: не загружена" } }, "th": { "stringUnit": { "state": "translated", - "value": "cmux ไม่สามารถผูกเซสชันที่แยกใหม่ได้อย่างปลอดภัย เริ่มการแยกอีกครั้งจากบานหน้าต่างหลัก" + "value": " ทีม: ยังไม่ได้โหลด" } }, "tr": { "stringUnit": { "state": "translated", - "value": "cmux yeni fork oturumunu güvenli bir şekilde bağlayamadı. Fork'u üst panelden yeniden başlatın." + "value": " ekip: yüklenmedi" } }, "uk": { "stringUnit": { "state": "translated", - "value": "cmux не вдалося безпечно прив’язати новий сеанс форка. Запустіть форк ще раз із батьківської панелі." + "value": " команда: не завантажена" } } } }, - "agent.codex.fork.notice.subtitle": { + "cli.auth.status.teamNotLoadedRetrying": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Fork unavailable" + "value": " team: not loaded (retrying)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "フォークを利用できません" + "value": " チーム: 未読み込み(再試行中)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": " 团队: 未加载(正在重试)" } }, "de": { "stringUnit": { "state": "translated", - "value": "Fork nicht verfügbar" + "value": " Team: nicht geladen (wird erneut versucht)" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Fork indisponible" + "value": " équipe: non chargée (nouvel essai)" } }, "ar": { "stringUnit": { "state": "translated", - "value": "التفريع غير متاح" + "value": " الفريق: لم يتم تحميله (جارٍ إعادة المحاولة)" } }, "es": { "stringUnit": { "state": "translated", - "value": "Fork no disponible" + "value": " equipo: no cargado (reintentando)" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法使用分支" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分支不可用" + "value": " 團隊: 未載入(重試中)" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포크를 사용할 수 없음" + "value": " 팀: 로드되지 않음 (재시도 중)" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Račvanje nije dostupno" + "value": " tim: nije učitan (ponovni pokušaj)" } }, "da": { "stringUnit": { "state": "translated", - "value": "Fork ikke tilgængelig" + "value": " team: ikke indlæst (prøver igen)" } }, "it": { "stringUnit": { "state": "translated", - "value": "Fork non disponibile" + "value": " team: non caricato (nuovo tentativo)" } }, "km": { "stringUnit": { "state": "translated", - "value": "មិនអាចប្រើការបំបែកបាន" + "value": " ក្រុម៖ មិនទាន់បានផ្ទុក (កំពុងព្យាយាមម្ដងទៀត)" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Fork utilgjengelig" + "value": " team: ikke lastet (prøver igjen)" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Fork niedostępny" + "value": " zespół: nie załadowano (ponawianie próby)" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Fork indisponível" + "value": " equipe: não carregada (tentando novamente)" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Форк недоступен" + "value": " команда: не загружена (повторная попытка)" } }, "th": { "stringUnit": { "state": "translated", - "value": "ไม่สามารถใช้การแยกสาขาได้" + "value": " ทีม: ยังไม่ได้โหลด (กำลังลองอีกครั้ง)" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Fork kullanılamıyor" + "value": " ekip: yüklenmedi (yeniden deneniyor)" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Форк недоступний" + "value": " команда: не завантажена (повторна спроба)" } } } }, - "agent.codex.fork.notice.title": { + "mobile.pairing.error.teamUnavailable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Agent fork" + "value": "Could not load your cmux team. Check your internet connection, then try again." } }, "ja": { "stringUnit": { "state": "translated", - "value": "エージェントフォーク" + "value": "cmux チームを読み込めませんでした。インターネット接続を確認してから、もう一度お試しください。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法加载你的 cmux 团队。请检查网络连接,然后重试。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Agent-Fork" + "value": "Dein cmux-Team konnte nicht geladen werden. Prüfe deine Internetverbindung und versuche es erneut." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Fork d’agent" + "value": "Impossible de charger votre équipe cmux. Vérifiez votre connexion Internet, puis réessayez." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تفريع الوكيل" + "value": "تعذّر تحميل فريق cmux الخاص بك. تحقّق من اتصالك بالإنترنت، ثم حاول مرة أخرى." } }, "es": { "stringUnit": { "state": "translated", - "value": "Fork del agente" + "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": "代理程式分支" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "代理分支" + "value": "無法載入你的 cmux 團隊。請檢查網路連線,然後再試一次。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "에이전트 포크" + "value": "cmux 팀을 불러올 수 없습니다. 인터넷 연결을 확인한 후 다시 시도하세요." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Račvanje agenta" + "value": "Nije moguće učitati vaš cmux tim. Provjerite internetsku vezu i pokušajte ponovo." } }, "da": { "stringUnit": { "state": "translated", - "value": "Agentfork" + "value": "Din cmux-team kunne ikke indlæses. Kontrollér din internetforbindelse, og prøv igen." } }, "it": { "stringUnit": { "state": "translated", - "value": "Fork dell'agente" + "value": "Impossibile caricare il tuo team cmux. Controlla la connessione Internet e riprova." } }, "km": { "stringUnit": { "state": "translated", - "value": "ការបំបែកភ្នាក់ងារ" + "value": "មិនអាចផ្ទុកក្រុម cmux របស់អ្នកបានទេ។ សូមពិនិត្យការតភ្ជាប់អ៊ីនធឺណិត ហើយព្យាយាមម្ដងទៀត។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Agentforgrening" + "value": "Kunne ikke laste inn cmux-teamet ditt. Kontroller internettilkoblingen, og prøv igjen." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Fork agenta" + "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": "Fork do agente" + "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": "Форк агента" + "value": "Не удалось загрузить вашу команду cmux. Проверьте подключение к интернету и попробуйте ещё раз." } }, "th": { "stringUnit": { "state": "translated", - "value": "การแยกสาขาของเอเจนต์" + "value": "ไม่สามารถโหลดทีม cmux ของคุณได้ ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองอีกครั้ง" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Ajan çatalı" + "value": "cmux ekibiniz yüklenemedi. İnternet bağlantınızı kontrol edip tekrar deneyin." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Форк агента" + "value": "Не вдалося завантажити вашу команду cmux. Перевірте підключення до інтернету й спробуйте ще раз." } } } }, - "socket.authTeam.committedStatusTimedOut": { + "command.newPaneAutoLayout.title": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "The team change was applied, but cmux did not report the updated status within 10 seconds. Run `cmux auth status` to confirm." + "value": "لوحة جديدة (تخطيط تلقائي)" } }, - "ja": { + "bs": { "stringUnit": { "state": "translated", - "value": "チームの変更は適用されましたが、cmux は 10 秒以内に更新後のステータスを報告しませんでした。`cmux auth status` を実行して確認してください。" + "value": "Novi panel (automatski raspored)" } }, - "zh-Hans": { + "da": { "stringUnit": { "state": "translated", - "value": "团队更改已应用,但 cmux 未能在 10 秒内报告更新后的状态。请运行 `cmux auth status` 确认。" + "value": "Nyt panel (automatisk layout)" } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Teamänderung wurde übernommen, aber cmux hat den aktualisierten Status nicht innerhalb von 10 Sekunden gemeldet. Führe `cmux auth status` aus, um es zu prüfen." + "value": "Neuer Bereich (automatisches Layout)" } }, - "fr": { + "en": { "stringUnit": { "state": "translated", - "value": "Le changement d’équipe a été appliqué, mais cmux n’a pas signalé le statut mis à jour dans les 10 secondes. Exécutez `cmux auth status` pour confirmer." + "value": "New Pane (Auto Layout)" } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "تم تطبيق تغيير الفريق، لكن cmux لم يُبلّغ عن الحالة المحدّثة خلال 10 ثوانٍ. شغّل `cmux auth status` للتأكيد." + "value": "Nuevo panel (diseño automático)" } }, - "es": { + "fr": { "stringUnit": { "state": "translated", - "value": "El cambio de equipo se aplicó, pero cmux no informó el estado actualizado en 10 segundos. Ejecuta `cmux auth status` para confirmarlo." + "value": "Nouveau panneau (disposition automatique)" } }, - "zh-Hant": { + "it": { "stringUnit": { "state": "translated", - "value": "團隊變更已套用,但 cmux 未能在 10 秒內回報更新後的狀態。請執行 `cmux auth status` 確認。" + "value": "Nuovo pannello (layout automatico)" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新しいペイン(自動レイアウト)" } }, "ko": { "stringUnit": { "state": "translated", - "value": "팀 변경은 적용되었지만 cmux가 10초 안에 업데이트된 상태를 보고하지 않았습니다. `cmux auth status`를 실행해 확인하세요." + "value": "새 패널 (자동 레이아웃)" } - } - } - }, - "settings.mobile.browserTunnel.allowOtherHosts": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "nb": { "stringUnit": { "state": "translated", - "value": "iOS Browser Reaches Other Hosts" + "value": "Nytt panel (automatisk oppsett)" } }, - "de": { + "pl": { "stringUnit": { "state": "translated", - "value": "iOS-Browser erreicht andere Hosts" + "value": "Nowy panel (układ automatyczny)" } }, - "fr": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Le navigateur iOS accède aux autres hôtes" + "value": "Novo Painel (Layout Automático)" } }, - "ar": { + "ru": { "stringUnit": { "state": "translated", - "value": "متصفح iOS يصل إلى مضيفين آخرين" + "value": "Новая панель (автоматическая раскладка)" } }, - "es": { + "th": { "stringUnit": { "state": "translated", - "value": "El navegador de iOS accede a otros hosts" + "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "iOS 瀏覽器可連線至其他主機" + "value": "Yeni Bölme (Otomatik Düzen)" } }, - "zh-Hans": { + "uk": { "stringUnit": { "state": "translated", - "value": "iOS 浏览器可访问其他主机" + "value": "Нова панель (автоматичне розташування)" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "iOS 브라우저의 다른 호스트 접근" + "value": "新建窗格(自动布局)" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "iOSブラウザから他のホストに接続" + "value": "新增窗格(自動佈局)" } } } }, - "settings.mobile.browserTunnel.allowOtherHosts.subtitleOn": { + "shortcut.newPaneAutoLayout.label": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "The iOS browser can load LAN, VPN, and internet hosts through this Mac. Link-local and cloud metadata addresses never go through this Mac; the phone loads them itself." + "value": "لوحة جديدة (تخطيط تلقائي)" } }, - "de": { + "bs": { "stringUnit": { "state": "translated", - "value": "Der iOS-Browser kann LAN-, VPN- und Internet-Hosts über diesen Mac laden. Link-Local- und Cloud-Metadatenadressen laufen nie über diesen Mac; das iPhone lädt sie selbst." + "value": "Novi panel (automatski raspored)" } }, - "fr": { + "da": { "stringUnit": { "state": "translated", - "value": "Le navigateur iOS peut charger des hôtes du réseau local, du VPN et d’Internet via ce Mac. Les adresses link-local et de métadonnées cloud ne passent jamais par ce Mac ; l’iPhone les charge lui-même." + "value": "Nyt panel (automatisk layout)" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "يمكن لمتصفح iOS تحميل مضيفي الشبكة المحلية وVPN والإنترنت عبر جهاز Mac هذا. لا تمر عناوين الارتباط المحلي وعناوين بيانات التعريف السحابية عبر جهاز Mac هذا أبدًا؛ يحمّلها iPhone بنفسه." + "value": "Neuer Bereich (automatisches Layout)" } }, - "es": { + "en": { "stringUnit": { "state": "translated", - "value": "El navegador de iOS puede cargar hosts de la red local, VPN e internet a través de este Mac. Las direcciones link-local y de metadatos en la nube nunca pasan por este Mac; el iPhone las carga por sí mismo." + "value": "New Pane (Auto Layout)" } }, - "zh-Hant": { + "es": { "stringUnit": { "state": "translated", - "value": "iOS 瀏覽器可透過這台 Mac 載入區域網路、VPN 與網際網路主機。連結本機位址與雲端中繼資料位址絕不經過這台 Mac,由 iPhone 自行載入。" + "value": "Nuevo panel (diseño automático)" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "iOS 浏览器可通过这台 Mac 加载局域网、VPN 和互联网主机。链路本地地址和云元数据地址绝不经过这台 Mac,由 iPhone 自行加载。" + "value": "Nouveau panneau (disposition automatique)" } }, - "ko": { + "it": { "stringUnit": { "state": "translated", - "value": "iOS 브라우저가 이 Mac을 통해 LAN, VPN, 인터넷 호스트를 불러올 수 있습니다. 링크 로컬 및 클라우드 메타데이터 주소는 이 Mac을 거치지 않으며 iPhone이 직접 불러옵니다." + "value": "Nuovo pannello (layout automatico)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "iOSブラウザはこのMac経由でLAN、VPN、インターネット上のホストを読み込めます。リンクローカルとクラウドメタデータのアドレスはこのMacを経由せず、iPhoneが直接読み込みます。" + "value": "新しいペイン(自動レイアウト)" } - } - } - }, - "settings.mobile.browserTunnel.allowOtherHosts.subtitleOff": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ko": { "stringUnit": { "state": "translated", - "value": "The iOS browser reaches only this Mac's localhost through this Mac. Other sites load over the phone's own network." + "value": "새 패널 (자동 레이아웃)" } }, - "de": { + "nb": { "stringUnit": { "state": "translated", - "value": "Der iOS-Browser erreicht über diesen Mac nur dessen localhost. Andere Websites werden über das eigene Netzwerk des Telefons geladen." + "value": "Nytt panel (automatisk oppsett)" } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Via ce Mac, le navigateur iOS n’accède qu’au localhost de ce Mac. Les autres sites se chargent via le réseau du téléphone." + "value": "Nowy panel (układ automatyczny)" } }, - "ar": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "لا يصل متصفح iOS عبر جهاز Mac هذا إلا إلى localhost الخاص به. تُحمَّل المواقع الأخرى عبر شبكة الهاتف نفسه." + "value": "Novo Painel (Layout Automático)" } }, - "es": { + "ru": { "stringUnit": { "state": "translated", - "value": "A través de este Mac, el navegador de iOS solo accede a su localhost. Los demás sitios se cargan con la red del propio teléfono." + "value": "Новая панель (автоматическая раскладка)" } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "iOS 瀏覽器只會透過這台 Mac 連線至它的 localhost。其他網站會透過手機本身的網路載入。" + "value": "บานหน้าต่างใหม่ (จัดวางอัตโนมัติ)" } }, - "zh-Hans": { + "tr": { "stringUnit": { "state": "translated", - "value": "iOS 浏览器只会通过这台 Mac 访问它的 localhost。其他网站通过手机自身的网络加载。" + "value": "Yeni Bölme (Otomatik Düzen)" } }, - "ko": { + "uk": { "stringUnit": { "state": "translated", - "value": "iOS 브라우저는 이 Mac을 통해 이 Mac의 localhost에만 접근합니다. 다른 사이트는 휴대폰 자체 네트워크로 불러옵니다." + "value": "Нова панель (автоматичне розташування)" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "iOSブラウザがこのMac経由で接続するのは、このMacのlocalhostだけです。他のサイトは電話自身のネットワークで読み込みます。" + "value": "新建窗格(自动布局)" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增窗格(自動佈局)" } } } }, - "settings.search.alias.setting.mobile.browserTunnelAllowOtherHosts": { + "cli.restoreSession.help": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security" + "value": "Usage: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nReopen the previous saved cmux session.\n\nIf the app is already running, this restores the last saved session into the current app.\nIf the app is not running, this launches cmux and lets startup restore reopen the saved session.\n\nEach cmux install (stable, nightly, rc, staging, tagged debug builds) saves its own session.\n--from Reopen another install's saved session in this running cmux, for example\n after trying nightly and switching back to stable. The session opens as\n additional windows; the other install's saved file is only read.\n--from Reopen a session file written by --export.\n--export Write this cmux's saved session to a file. Pass --force to replace an\n existing file.\n\n--from and --export require cmux to be running." } }, "de": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security mobil browser tunnel proxy lokales netzwerk hosts netzwerk sicherheit" + "value": "Verwendung: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nDie zuletzt gespeicherte cmux-Sitzung erneut öffnen.\n\nLäuft die App bereits, wird die zuletzt gespeicherte Sitzung in der aktuellen App wiederhergestellt.\nLäuft die App nicht, wird cmux gestartet und die gespeicherte Sitzung beim Start wiederhergestellt.\n\nJede cmux-Installation (stable, nightly, rc, staging, getaggte Debug-Builds) speichert ihre eigene Sitzung.\n--from Die gespeicherte Sitzung einer anderen Installation in diesem laufenden cmux öffnen,\n z. B. nachdem Sie nightly ausprobiert haben und zu stable zurückkehren. Die Sitzung\n öffnet sich in zusätzlichen Fenstern; die Datei der anderen Installation wird nur gelesen.\n--from Eine mit --export geschriebene Sitzungsdatei öffnen.\n--export Die gespeicherte Sitzung dieses cmux in eine Datei schreiben. Mit --force wird eine\n vorhandene Datei ersetzt.\n\n--from und --export setzen ein laufendes cmux voraus." } }, "fr": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security mobile navigateur tunnel proxy réseau local hôtes réseau sécurité" + "value": "Utilisation : cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nRouvrir la dernière session cmux enregistrée.\n\nSi l'app est déjà ouverte, la dernière session enregistrée est restaurée dans l'app actuelle.\nSi l'app n'est pas ouverte, cmux est lancé et la restauration au démarrage rouvre la session enregistrée.\n\nChaque installation de cmux (stable, nightly, rc, staging, builds debug avec tag) enregistre sa propre session.\n--from Rouvrir la session enregistrée d'une autre installation dans ce cmux en cours, par exemple\n après avoir essayé nightly puis être revenu à stable. La session s'ouvre dans des fenêtres\n supplémentaires ; le fichier de l'autre installation est seulement lu.\n--from Rouvrir un fichier de session écrit par --export.\n--export Écrire la session enregistrée de ce cmux dans un fichier. Ajoutez --force pour remplacer\n un fichier existant.\n\n--from et --export nécessitent que cmux soit ouvert." } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security متصفح نفق وكيل شبكة محلية مضيفون شبكة أمان" + "value": "Uso: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nVuelve a abrir la última sesión guardada de cmux.\n\nSi la app ya está abierta, restaura la última sesión guardada en la app actual.\nSi la app no está abierta, inicia cmux y deja que la restauración de inicio vuelva a abrir la sesión guardada.\n\nCada instalación de cmux (stable, nightly, rc, staging, builds de depuración con etiqueta) guarda su propia sesión.\n--from Abre en este cmux la sesión guardada de otra instalación, por ejemplo después de\n probar nightly y volver a stable. La sesión se abre en ventanas adicionales; el archivo\n de la otra instalación solo se lee.\n--from Abre un archivo de sesión escrito con --export.\n--export Escribe en un archivo la sesión guardada de este cmux. Usa --force para reemplazar un\n archivo existente.\n\n--from y --export requieren que cmux esté abierto." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security móvil navegador túnel proxy red local hosts red seguridad" + "value": "使い方: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n前回保存した cmux セッションを再度開きます。\n\nアプリがすでに起動している場合は、最後に保存したセッションを現在のアプリに復元します。\nアプリが起動していない場合は、cmux を起動し、起動時の復元で保存済みセッションを開きます。\n\ncmux の各インストール(stable、nightly、rc、staging、タグ付き debug ビルド)はそれぞれ独自のセッションを保存します。\n--from 別のインストールで保存したセッションを、起動中のこの cmux で開きます。たとえば nightly を\n 試したあと stable に戻る場合に使います。セッションは追加のウインドウとして開き、別の\n インストールのファイルは読み取るだけです。\n--from --export で書き出したセッションファイルを開きます。\n--export この cmux の保存済みセッションをファイルに書き出します。既存のファイルを置き換えるには\n --force を指定します。\n\n--from と --export を使うには cmux が起動している必要があります。" } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security 行動裝置 瀏覽器 通道 代理 區域網路 主機 網路 安全性" + "value": "الاستخدام: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\nأعد فتح آخر جلسة cmux محفوظة.\n\nإذا كان التطبيق يعمل بالفعل، فستُستعاد آخر جلسة محفوظة في التطبيق الحالي.\nإذا لم يكن التطبيق يعمل، فسيُشغَّل cmux وتعيد استعادة بدء التشغيل فتح الجلسة المحفوظة.\n\nيحفظ كل تثبيت من cmux (stable وnightly وrc وstaging وإصدارات debug الموسومة) جلسته الخاصة.\n--from افتح الجلسة المحفوظة لتثبيت آخر في cmux الذي يعمل الآن، مثلًا\n بعد تجربة nightly والعودة إلى stable. تُفتح الجلسة في\n نوافذ إضافية، ويُقرأ ملف التثبيت الآخر فقط.\n--from افتح ملف جلسة كتبه الأمر --export.\n--export اكتب الجلسة المحفوظة لهذا cmux في ملف. استخدم --force لاستبدال\n ملف موجود.\n\nيتطلب --from و--export أن يكون cmux قيد التشغيل." } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security 移动 浏览器 隧道 代理 局域网 主机 网络 安全" + "value": "用法: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n重新開啟上次儲存的 cmux 工作階段。\n\n如果 App 已在執行,會將上次儲存的工作階段還原到目前的 App 中。\n如果 App 未執行,會啟動 cmux,並由啟動還原重新開啟已儲存的工作階段。\n\n每個 cmux 安裝(stable、nightly、rc、staging、帶標籤的 debug 建置)都會儲存自己的工作階段。\n--from 在執行中的 cmux 開啟另一個安裝儲存的工作階段,例如\n 試用 nightly 後切回 stable。工作階段會以\n 額外視窗開啟;另一個安裝的檔案只會被讀取。\n--from 開啟由 --export 寫出的工作階段檔案。\n--export 將此 cmux 儲存的工作階段寫入檔案。使用 --force 可取代\n 既有檔案。\n\n--from 和 --export 需要 cmux 正在執行。" } }, - "ko": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security 모바일 브라우저 터널 프록시 로컬 네트워크 호스트 네트워크 보안" + "value": "用法: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n重新打开上次保存的 cmux 会话。\n\n如果应用已在运行,会将上次保存的会话恢复到当前应用中。\n如果应用未运行,会启动 cmux,并由启动恢复重新打开保存的会话。\n\n每个 cmux 安装(stable、nightly、rc、staging、带标签的 debug 构建)都会保存自己的会话。\n--from 在正在运行的 cmux 中打开另一个安装保存的会话,例如\n 试用 nightly 后切回 stable。会话会以\n 额外窗口打开;另一个安装的会话文件只会被读取。\n--from 打开由 --export 写出的会话文件。\n--export 将此 cmux 保存的会话写入文件。使用 --force 可替换\n 已有文件。\n\n--from 和 --export 需要 cmux 正在运行。" } }, - "ja": { + "ko": { "stringUnit": { "state": "translated", - "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security モバイル ブラウザ トンネル プロキシ ローカルネットワーク ホスト ネットワーク セキュリティ" + "value": "사용법: cmux restore-session\n cmux restore-session --from |path>\n cmux restore-session --export [--force]\n\n마지막으로 저장된 cmux 세션을 다시 엽니다.\n\n앱이 이미 실행 중이면 마지막으로 저장된 세션을 현재 앱에 복원합니다.\n앱이 실행 중이 아니면 cmux를 실행하고 시작 복원이 저장된 세션을 다시 열도록 합니다.\n\n각 cmux 설치(stable, nightly, rc, staging, 태그가 지정된 debug 빌드)는 자체 세션을 저장합니다.\n--from 다른 설치에 저장된 세션을 실행 중인 이 cmux에서 엽니다. 예를 들어\n nightly를 사용해 본 뒤 stable로 돌아올 때 사용합니다. 세션은\n 추가 창으로 열리며, 다른 설치의 파일은 읽기만 합니다.\n--from --export로 작성한 세션 파일을 엽니다.\n--export 이 cmux에 저장된 세션을 파일로 씁니다. 기존 파일을 바꾸려면\n --force를 사용하세요.\n\n--from과 --export를 사용하려면 cmux가 실행 중이어야 합니다." } } } }, - "cli.browser.inputFiles.error.usage": { + "session.export.error.destinationExists": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "set-input-files requires --selector and either --file (repeatable) or --clear" + "value": "%@ already exists. Pass --force to replace it." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يتطلب set-input-files الخيار --selector مع --file (يمكن تكراره) أو --clear" + "value": "%@ موجود بالفعل. استخدم --force لاستبداله." } }, "de": { "stringUnit": { "state": "translated", - "value": "set-input-files benötigt --selector und entweder --file (wiederholbar) oder --clear" + "value": "%@ ist bereits vorhanden. Verwenden Sie --force, um die Datei zu ersetzen." } }, "es": { "stringUnit": { "state": "translated", - "value": "set-input-files requiere --selector y --file (repetible) o --clear" + "value": "%@ ya existe. Usa --force para reemplazarlo." } }, "fr": { "stringUnit": { "state": "translated", - "value": "set-input-files nécessite --selector et soit --file (répétable), soit --clear" + "value": "%@ existe déjà. Ajoutez --force pour le remplacer." } }, "ja": { "stringUnit": { "state": "translated", - "value": "set-input-files には --selector と、--file (複数指定可)または --clear が必要です" + "value": "%@ はすでに存在します。置き換えるには --force を指定してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "set-input-files에는 --selector 와 --file (반복 가능) 또는 --clear가 필요합니다" + "value": "%@ 파일이 이미 있습니다. 바꾸려면 --force를 사용하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "set-input-files 需要 --selector ,以及 --file (可重复)或 --clear" + "value": "%@ 已存在。使用 --force 以替换它。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "set-input-files 需要 --selector ,以及 --file (可重複)或 --clear" + "value": "%@ 已存在。使用 --force 以取代它。" } } } }, - "cli.browser.inputFiles.help": { + "session.export.error.destinationIsLive": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Select local files (up to 128 files, 32 MiB total) or clear the selection. Directory uploads are unsupported." + "value": "%@ is this cmux's own session file. Choose another path." } }, "ar": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n اختر ملفات محلية (حتى 128 ملفًا بإجمالي 32 MiB) أو امسح الاختيار. رفع المجلدات غير مدعوم." + "value": "%@ هو ملف الجلسة الخاص بهذا الـ cmux. اختر مسارًا آخر." } }, "de": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Lokale Dateien auswählen (bis zu 128 Dateien, insgesamt 32 MiB) oder die Auswahl leeren. Ordner-Uploads werden nicht unterstützt." + "value": "%@ ist die eigene Sitzungsdatei dieses cmux. Wählen Sie einen anderen Pfad." } }, "es": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Selecciona archivos locales (hasta 128 archivos, 32 MiB en total) o borra la selección. No se admite subir carpetas." + "value": "%@ es el archivo de sesión de este cmux. Elige otra ruta." } }, "fr": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Sélectionner des fichiers locaux (jusqu’à 128 fichiers, 32 MiB au total) ou effacer la sélection. L’envoi de dossiers n’est pas pris en charge." + "value": "%@ est le fichier de session de ce cmux. Choisissez un autre chemin." } }, "ja": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n ローカルファイルを選択(最大128個、合計32 MiB)するか、選択を解除します。ディレクトリのアップロードには対応していません。" + "value": "%@ はこの cmux 自身のセッションファイルです。別のパスを指定してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n 로컬 파일을 선택하거나(최대 128개, 총 32 MiB) 선택을 지웁니다. 디렉터리 업로드는 지원되지 않습니다." + "value": "%@ 파일은 이 cmux의 세션 파일입니다. 다른 경로를 선택하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n 选择本地文件(最多 128 个文件,总计 32 MiB)或清除选择。不支持上传目录。" + "value": "%@ 是此 cmux 自己的会话文件。请选择其他路径。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n 選取本機檔案(最多 128 個檔案,總計 32 MiB)或清除選取。不支援上傳目錄。" + "value": "%@ 是此 cmux 本身的工作階段檔案。請選擇其他路徑。" } } } }, - "browser.inputFiles.error.invalidTarget": { + "session.export.error.noSnapshot": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The selector must match a file input without directory selection" + "value": "cmux has not saved a session yet. Try again in a few seconds." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب أن يطابق المحدد حقل إدخال ملفات لا يستخدم اختيار المجلدات" + "value": "لم يحفظ cmux أي جلسة بعد. حاول مرة أخرى بعد بضع ثوانٍ." } }, "de": { "stringUnit": { "state": "translated", - "value": "Der Selektor muss ein Dateieingabefeld ohne Ordnerauswahl treffen" + "value": "cmux hat noch keine Sitzung gespeichert. Versuchen Sie es in ein paar Sekunden erneut." } }, "es": { "stringUnit": { "state": "translated", - "value": "El selector debe coincidir con un campo de archivos sin selección de carpetas" + "value": "cmux aún no ha guardado ninguna sesión. Inténtalo de nuevo en unos segundos." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Le sélecteur doit correspondre à un champ de fichier sans sélection de dossier" + "value": "cmux n'a pas encore enregistré de session. Réessayez dans quelques secondes." } }, "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 尚未儲存工作階段。請過幾秒再試一次。" } } } }, - "browser.inputFiles.error.multipleRequired": { + "session.export.error.writeFailed": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This file input does not allow multiple files" + "value": "Could not write %@." } }, "ar": { "stringUnit": { "state": "translated", - "value": "لا يسمح حقل إدخال الملفات هذا باختيار عدة ملفات" + "value": "تعذّرت كتابة %@." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieses Dateieingabefeld erlaubt nicht mehrere Dateien" + "value": "%@ konnte nicht geschrieben werden." } }, "es": { "stringUnit": { "state": "translated", - "value": "Este campo de archivos no permite seleccionar varios archivos" + "value": "No se pudo escribir %@." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ce champ de fichier ne permet pas de sélectionner plusieurs fichiers" + "value": "Impossible d'écrire %@." } }, "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": "無法寫入 %@。" } } } }, - "browser.inputFiles.error.cancelled": { + "session.import.error.fileNotFound": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "File upload preparation was cancelled" + "value": "No saved cmux session at %@." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تم إلغاء تجهيز الملفات للرفع" + "value": "لا توجد جلسة cmux محفوظة في %@." } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Vorbereitung des Datei-Uploads wurde abgebrochen" + "value": "Unter %@ gibt es keine gespeicherte cmux-Sitzung." } }, "es": { "stringUnit": { "state": "translated", - "value": "Se canceló la preparación de los archivos para subir" + "value": "No hay ninguna sesión de cmux guardada en %@." } }, "fr": { "stringUnit": { "state": "translated", - "value": "La préparation des fichiers à envoyer a été annulée" + "value": "Aucune session cmux enregistrée à l'emplacement %@." } }, "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 工作階段。" } } } }, - "browser.inputFiles.error.invalidParameters": { + "session.import.error.newerSchema": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "set-input-files requires a selector and a files array of absolute paths" + "value": "%1$@ was saved by a newer cmux (session format %2$@, this cmux reads %3$@). Update cmux to import it." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يتطلب set-input-files محددًا ومصفوفة files تحتوي على مسارات مطلقة" + "value": "حُفظ %1$@ بواسطة إصدار أحدث من cmux (تنسيق الجلسة %2$@، ويقرأ هذا الـ cmux التنسيق %3$@). حدّث cmux لاستيراده." } }, "de": { "stringUnit": { "state": "translated", - "value": "set-input-files benötigt einen Selektor und ein files-Array mit absoluten Pfaden" + "value": "%1$@ wurde von einem neueren cmux gespeichert (Sitzungsformat %2$@, dieses cmux liest %3$@). Aktualisieren Sie cmux, um die Datei zu importieren." } }, "es": { "stringUnit": { "state": "translated", - "value": "set-input-files requiere un selector y una matriz files de rutas absolutas" + "value": "%1$@ se guardó con un cmux más reciente (formato de sesión %2$@; este cmux lee el %3$@). Actualiza cmux para importarlo." } }, "fr": { "stringUnit": { "state": "translated", - "value": "set-input-files nécessite un sélecteur et un tableau files de chemins absolus" + "value": "%1$@ a été enregistré par une version plus récente de cmux (format de session %2$@, ce cmux lit le format %3$@). Mettez cmux à jour pour l'importer." } }, "ja": { "stringUnit": { "state": "translated", - "value": "set-input-files にはセレクターと絶対パスを含む files 配列が必要です" + "value": "%1$@ は新しい cmux で保存されています(セッション形式 %2$@、この cmux が読める形式は %3$@)。読み込むには cmux をアップデートしてください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "set-input-files에는 선택자와 절대 경로로 구성된 files 배열이 필요합니다" + "value": "%1$@ 파일은 더 새로운 cmux에서 저장되었습니다(세션 형식 %2$@, 이 cmux는 %3$@ 형식을 읽음). 가져오려면 cmux를 업데이트하세요." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "set-input-files 需要选择器及包含绝对路径的 files 数组" + "value": "%1$@ 由较新版本的 cmux 保存(会话格式 %2$@,此 cmux 读取 %3$@)。请更新 cmux 后再导入。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "set-input-files 需要選取器及包含絕對路徑的 files 陣列" + "value": "%1$@ 是由較新版本的 cmux 儲存(工作階段格式 %2$@,此 cmux 讀取 %3$@)。請更新 cmux 後再匯入。" } } } }, - "browser.inputFiles.error.invalidSelection": { + "session.import.error.noWindows": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Select at most 128 files using absolute paths" + "value": "%@ has no windows to restore." } }, "ar": { "stringUnit": { "state": "translated", - "value": "اختر ما يصل إلى 128 ملفًا باستخدام مسارات مطلقة" + "value": "لا يحتوي %@ على نوافذ لاستعادتها." } }, "de": { "stringUnit": { "state": "translated", - "value": "Wählen Sie höchstens 128 Dateien mit absoluten Pfaden aus" + "value": "%@ enthält keine Fenster zum Wiederherstellen." } }, "es": { "stringUnit": { "state": "translated", - "value": "Selecciona como máximo 128 archivos mediante rutas absolutas" + "value": "%@ no tiene ventanas que restaurar." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Sélectionnez au maximum 128 fichiers avec des chemins absolus" + "value": "%@ ne contient aucune fenêtre à restaurer." } }, "ja": { "stringUnit": { "state": "translated", - "value": "絶対パスで最大128個のファイルを選択してください" + "value": "%@ には復元できるウインドウがありません。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "절대 경로를 사용하여 최대 128개의 파일을 선택하세요" + "value": "%@ 파일에 복원할 창이 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "请使用绝对路径选择最多 128 个文件" + "value": "%@ 没有可恢复的窗口。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "請使用絕對路徑選取最多 128 個檔案" + "value": "%@ 沒有可還原的視窗。" } } } }, - "browser.inputFiles.error.timeout": { + "session.import.error.notASnapshot": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Timed out preparing files for upload" + "value": "%@ is not a cmux session snapshot." } }, "ar": { "stringUnit": { "state": "translated", - "value": "انتهت مهلة تجهيز الملفات للرفع" + "value": "%@ ليس لقطة جلسة cmux." } }, "de": { "stringUnit": { "state": "translated", - "value": "Zeitüberschreitung beim Vorbereiten der Dateien für den Upload" + "value": "%@ ist keine cmux-Sitzungsdatei." } }, "es": { "stringUnit": { "state": "translated", - "value": "Se agotó el tiempo de preparación de los archivos para subir" + "value": "%@ no es una instantánea de sesión de cmux." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Le délai de préparation des fichiers à envoyer a été dépassé" + "value": "%@ n'est pas un instantané de session 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 工作階段快照。" } } } }, - "browser.inputFiles.error.tooLarge": { + "session.import.error.nothingRestored": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The combined upload must be no larger than 32 MiB" + "value": "Nothing in %@ could be reopened." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب ألا يتجاوز الحجم الإجمالي للملفات المرفوعة 32 MiB" + "value": "تعذّر إعادة فتح أي شيء من %@." } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Uploads dürfen insgesamt nicht größer als 32 MiB sein" + "value": "Aus %@ konnte nichts wieder geöffnet werden." } }, "es": { "stringUnit": { "state": "translated", - "value": "El tamaño total de los archivos para subir no debe superar los 32 MiB" + "value": "No se pudo volver a abrir nada de %@." } }, "fr": { "stringUnit": { "state": "translated", - "value": "La taille totale des fichiers à envoyer ne doit pas dépasser 32 MiB" + "value": "Aucun élément de %@ n'a pu être rouvert." } }, "ja": { "stringUnit": { "state": "translated", - "value": "アップロードするファイルの合計サイズは32 MiB以下にしてください" + "value": "%@ から再度開けるものはありませんでした。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "전체 업로드 크기는 32 MiB를 초과할 수 없습니다" + "value": "%@ 파일에서 다시 열 수 있는 항목이 없습니다." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "上传文件的总大小不得超过 32 MiB" + "value": "无法重新打开 %@ 中的任何内容。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "上傳檔案的總大小不得超過 32 MiB" + "value": "無法重新開啟 %@ 中的任何內容。" } } } }, - "browser.inputFiles.error.unreadableFile": { + "session.import.error.olderSchema": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Every upload path must be a readable regular file" + "value": "%1$@ uses an older session format (%2$@) that this cmux no longer reads (%3$@)." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يجب أن يشير كل مسار للرفع إلى ملف عادي قابل للقراءة" + "value": "يستخدم %1$@ تنسيق جلسة أقدم (%2$@) لم يعد هذا الـ cmux يقرؤه (%3$@)." } }, "de": { "stringUnit": { "state": "translated", - "value": "Jeder Upload-Pfad muss auf eine lesbare reguläre Datei verweisen" + "value": "%1$@ verwendet ein älteres Sitzungsformat (%2$@), das dieses cmux nicht mehr liest (%3$@)." } }, "es": { "stringUnit": { "state": "translated", - "value": "Cada ruta de subida debe apuntar a un archivo normal que se pueda leer" + "value": "%1$@ usa un formato de sesión anterior (%2$@) que este cmux ya no lee (%3$@)." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Chaque chemin à envoyer doit désigner un fichier ordinaire lisible" + "value": "%1$@ utilise un ancien format de session (%2$@) que ce cmux ne lit plus (%3$@)." } }, "ja": { "stringUnit": { "state": "translated", - "value": "各アップロードパスは読み取り可能な通常ファイルである必要があります" + "value": "%1$@ はこの cmux が読み込まなくなった古いセッション形式(%2$@)を使用しています(%3$@)。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "모든 업로드 경로는 읽을 수 있는 일반 파일이어야 합니다" + "value": "%1$@ 파일은 이 cmux가 더 이상 읽지 않는 이전 세션 형식(%2$@)을 사용합니다(%3$@)." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "每个上传路径都必须指向可读取的普通文件" + "value": "%1$@ 使用较旧的会话格式(%2$@),此 cmux 已不再读取(%3$@)。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "每個上傳路徑都必須指向可讀取的一般檔案" + "value": "%1$@ 使用較舊的工作階段格式(%2$@),此 cmux 已不再讀取(%3$@)。" } } } }, - "cloudTree.group.cloudMachines.usage": { + "session.import.error.unknownChannel": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "%1$d/%2$d" + "value": "Unknown cmux channel \"%@\". Use stable, nightly, rc, staging, debug:, or a path to a session file." } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "قناة cmux غير معروفة \"%@\". استخدم stable أو nightly أو rc أو staging أو debug: أو مسار ملف جلسة." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "Unbekannter cmux-Kanal \"%@\". Verwenden Sie stable, nightly, rc, staging, debug: oder einen Pfad zu einer Sitzungsdatei." } }, "es": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "Canal de cmux desconocido \"%@\". Usa stable, nightly, rc, staging, debug: o la ruta de un archivo de sesión." } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "Canal cmux inconnu « %@ ». Utilisez stable, nightly, rc, staging, debug: ou le chemin d'un fichier de session." } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "不明な cmux チャンネル「%@」です。stable、nightly、rc、staging、debug:、またはセッションファイルのパスを指定してください。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%1$d/%2$d" + "value": "알 수 없는 cmux 채널 \"%@\"입니다. stable, nightly, rc, staging, debug: 또는 세션 파일 경로를 사용하세요." } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "未知的 cmux 渠道“%@”。请使用 stable、nightly、rc、staging、debug: 或会话文件路径。" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "未知的 cmux 通道「%@」。請使用 stable、nightly、rc、staging、debug: 或工作階段檔案路徑。" } - }, - "it": { + } + } + }, + "session.import.error.unreadable": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "Could not read %@." } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "تعذّرت قراءة %@." } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "%@ konnte nicht gelesen werden." } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "No se pudo leer %@." } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "Impossible de lire %@." } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "%@ を読み込めませんでした。" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "%@ 파일을 읽을 수 없습니다." } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "无法读取 %@。" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "%1$d/%2$d" + "value": "無法讀取 %@。" } } } }, - "cloud.ports.vpnOff.explanation": { + "session.import.error.liveSnapshot": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Ports open in cmux without setup. To use private addresses in other apps, set up Cloud VPN." + "value": "%@ is the session this cmux is saving right now. Run cmux restore-session without --from to reopen the previous launch." } }, "de": { "stringUnit": { "state": "translated", - "value": "Ports lassen sich in cmux ohne Einrichtung öffnen. Richte Cloud-VPN ein, um private Adressen in anderen Apps zu verwenden." + "value": "%@ ist die Sitzung, die dieses cmux gerade speichert. Führen Sie cmux restore-session ohne --from aus, um den vorherigen Start wieder zu öffnen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les ports s’ouvrent dans cmux sans configuration. Pour utiliser les adresses privées dans d’autres apps, configurez le VPN Cloud." + "value": "%@ est la session que ce cmux enregistre actuellement. Exécutez cmux restore-session sans --from pour rouvrir le lancement précédent." } }, "ar": { "stringUnit": { "state": "translated", - "value": "تُفتح المنافذ في cmux دون إعداد. لاستخدام العناوين الخاصة في التطبيقات الأخرى، أعدّ Cloud VPN." + "value": "%@ هي الجلسة التي يحفظها cmux هذا الآن. شغّل cmux restore-session دون --from لإعادة فتح التشغيل السابق." } }, "es": { "stringUnit": { "state": "translated", - "value": "Los puertos se abren en cmux sin configuración. Para usar direcciones privadas en otras apps, configura Cloud VPN." + "value": "%@ es la sesión que este cmux está guardando ahora. Ejecuta cmux restore-session sin --from para volver a abrir el inicio anterior." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無需設定即可在 cmux 中開啟連接埠。如要在其他 App 使用私有位址,請設定 Cloud VPN。" + "value": "%@ 是此 cmux 目前正在儲存的工作階段。執行不含 --from 的 cmux restore-session 以重新開啟上次啟動的內容。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无需设置即可在 cmux 中打开端口。要在其他应用中使用私有地址,请设置云 VPN。" + "value": "%@ 是此 cmux 当前正在保存的会话。运行不带 --from 的 cmux restore-session 以重新打开上次启动的内容。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "별도 설정 없이 cmux에서 포트를 열 수 있습니다. 다른 앱에서 비공개 주소를 사용하려면 Cloud VPN을 설정하세요." + "value": "%@ 파일은 이 cmux가 지금 저장 중인 세션입니다. 이전 실행을 다시 열려면 --from 없이 cmux restore-session을 실행하세요." } }, "ja": { "stringUnit": { "state": "translated", - "value": "設定なしで cmux 内のポートを開けます。他のアプリでプライベートアドレスを使うには、Cloud VPN を設定してください。" + "value": "%@ はこの cmux が現在保存しているセッションです。前回の起動を再度開くには、--from を付けずに cmux restore-session を実行してください。" } - }, - "bs": { + } + } + }, + "machines.requiresPro.upgrade.short": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { - "state": "needs_review", - "value": "Portovi se otvaraju u cmux-u bez podešavanja. Za korištenje privatnih adresa u drugim aplikacijama podesite Cloud VPN." + "state": "translated", + "value": "Upgrade" } }, - "da": { + "ar": { "stringUnit": { - "state": "needs_review", - "value": "Porte åbnes i cmux uden opsætning. Opsæt Cloud VPN for at bruge private adresser i andre apps." + "state": "translated", + "value": "ترقية" } }, - "it": { + "de": { "stringUnit": { - "state": "needs_review", - "value": "Le porte si aprono in cmux senza configurazione. Per usare gli indirizzi privati in altre app, configura Cloud VPN." + "state": "translated", + "value": "Upgraden" } }, - "km": { + "es": { "stringUnit": { - "state": "needs_review", - "value": "ច្រកអាចបើកក្នុង cmux ដោយមិនចាំបាច់រៀបចំ។ ដើម្បីប្រើអាសយដ្ឋានឯកជនក្នុងកម្មវិធីផ្សេងទៀត សូមរៀបចំ Cloud VPN។" + "state": "translated", + "value": "Actualizar" } }, - "nb": { + "fr": { "stringUnit": { - "state": "needs_review", - "value": "Porter åpnes i cmux uten oppsett. Sett opp Cloud VPN for å bruke private adresser i andre apper." + "state": "translated", + "value": "Mettre à niveau" } }, - "pl": { + "ja": { "stringUnit": { - "state": "needs_review", - "value": "Porty otwierają się w cmux bez konfiguracji. Aby używać prywatnych adresów w innych aplikacjach, skonfiguruj Cloud VPN." + "state": "translated", + "value": "アップグレード" } }, - "pt-BR": { + "ko": { "stringUnit": { - "state": "needs_review", - "value": "As portas abrem no cmux sem configuração. Para usar endereços privados em outros apps, configure o Cloud VPN." + "state": "translated", + "value": "업그레이드" } }, - "ru": { + "zh-Hans": { "stringUnit": { - "state": "needs_review", - "value": "Порты открываются в cmux без настройки. Чтобы использовать приватные адреса в других приложениях, настройте Cloud VPN." + "state": "translated", + "value": "升级" } }, - "th": { + "zh-Hant": { "stringUnit": { - "state": "needs_review", - "value": "เปิดพอร์ตใน cmux ได้โดยไม่ต้องตั้งค่า หากต้องการใช้ที่อยู่ส่วนตัวในแอปอื่น ให้ตั้งค่า Cloud VPN" - } - }, - "tr": { - "stringUnit": { - "state": "needs_review", - "value": "Bağlantı noktaları cmux içinde kurulum gerektirmeden açılır. Diğer uygulamalarda özel adresleri kullanmak için Cloud VPN’i kurun." - } - }, - "uk": { - "stringUnit": { - "state": "needs_review", - "value": "Порти відкриваються в cmux без налаштування. Щоб використовувати приватні адреси в інших застосунках, налаштуйте Cloud VPN." + "state": "translated", + "value": "升級" } } } }, - "cloud.ports.vpnOff.title": { + "machines.sessionRejected.signInAgain.short": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Optional: Cloud VPN" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Optional: Cloud-VPN" + "value": "Sign In" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Facultatif : VPN Cloud" + "value": "تسجيل الدخول" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "اختياري: Cloud VPN" + "value": "Anmelden" } }, "es": { "stringUnit": { "state": "translated", - "value": "Opcional: Cloud VPN" + "value": "Iniciar sesión" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "選用:Cloud VPN" + "value": "Se reconnecter" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "可选:云 VPN" + "value": "再サインイン" } }, "ko": { "stringUnit": { "state": "translated", - "value": "선택 사항: Cloud VPN" + "value": "다시 로그인" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "任意:Cloud VPN" - } - }, - "bs": { - "stringUnit": { - "state": "needs_review", - "value": "Opcionalno: Cloud VPN" + "value": "重新登录" } }, - "da": { + "zh-Hant": { "stringUnit": { - "state": "needs_review", - "value": "Valgfrit: Cloud VPN" + "state": "translated", + "value": "重新登入" } - }, - "it": { + } + } + }, + "machines.requiresPro.stale": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { - "state": "needs_review", - "value": "Facoltativo: Cloud VPN" + "state": "translated", + "value": "Cloud machines need cmux Pro, showing last known" } }, - "km": { + "ar": { "stringUnit": { - "state": "needs_review", - "value": "ជាជម្រើស៖ Cloud VPN" + "state": "translated", + "value": "تتطلب الأجهزة السحابية إصدار cmux Pro، يتم عرض آخر حالة معروفة" } }, - "nb": { + "de": { "stringUnit": { - "state": "needs_review", - "value": "Valgfritt: Cloud VPN" + "state": "translated", + "value": "Cloud-Maschinen erfordern cmux Pro, zeige letzten bekannten Stand" } }, - "pl": { + "es": { "stringUnit": { - "state": "needs_review", - "value": "Opcjonalnie: Cloud VPN" + "state": "translated", + "value": "Las máquinas en la nube requieren cmux Pro, se muestra el último estado conocido" } }, - "pt-BR": { + "fr": { "stringUnit": { - "state": "needs_review", - "value": "Opcional: Cloud VPN" + "state": "translated", + "value": "Les machines Cloud nécessitent cmux Pro, affichage des dernières informations connues" } }, - "ru": { + "ja": { "stringUnit": { - "state": "needs_review", - "value": "Необязательно: Cloud VPN" + "state": "translated", + "value": "Cloud マシンには cmux Pro が必要です。最後の状態を表示中" } }, - "th": { + "ko": { "stringUnit": { - "state": "needs_review", - "value": "ตัวเลือกเพิ่มเติม: Cloud VPN" + "state": "translated", + "value": "클라우드 머신을 사용하려면 cmux Pro가 필요함, 마지막으로 확인된 상태 표시 중" } }, - "tr": { + "zh-Hans": { "stringUnit": { - "state": "needs_review", - "value": "İsteğe bağlı: Cloud VPN" + "state": "translated", + "value": "云端机器需要 cmux Pro,显示的是最后已知状态" } }, - "uk": { + "zh-Hant": { "stringUnit": { - "state": "needs_review", - "value": "Необов’язково: Cloud VPN" + "state": "translated", + "value": "雲端機器需要 cmux Pro,顯示上次已知的狀態" } } } }, - "cloud.vpn.setup.address.body": { + "machines.sessionRejected.stale": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Each machine has its own private IP. Two machines may both expose port 3000, for example 10.40.0.10:3000 and 10.40.0.11:3000. Right-click a Cloud port and choose Copy Private Address URL after connecting. These addresses are private, not public share links." + "value": "Sign-in needs a refresh, showing last known" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Jede Maschine hat eine eigene private IP. Zwei Maschinen können beide Port 3000 verwenden, etwa 10.40.0.10:3000 und 10.40.0.11:3000. Klicken Sie nach dem Verbinden mit der rechten Maustaste auf einen Cloud-Port und wählen Sie URL der privaten Adresse kopieren. Diese Adressen sind privat und keine öffentlichen Freigabelinks." + "value": "يحتاج تسجيل الدخول إلى تحديث، يتم عرض آخر حالة معروفة" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "Chaque machine a sa propre IP privée. Deux machines peuvent utiliser le port 3000, par exemple 10.40.0.10:3000 et 10.40.0.11:3000. Après connexion, faites un clic droit sur un port Cloud et choisissez Copier l’URL de l’adresse privée. Ces adresses sont privées, ce ne sont pas des liens publics de partage." + "value": "Anmeldung muss aktualisiert werden, zeige letzten bekannten Stand" } }, "es": { "stringUnit": { "state": "translated", - "value": "Cada máquina tiene su propia IP privada. Dos máquinas pueden usar el puerto 3000, como 10.40.0.10:3000 y 10.40.0.11:3000. Después de conectar, haz clic derecho en un puerto Cloud y elige Copiar URL de dirección privada. Estas direcciones son privadas, no enlaces públicos para compartir." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "各マシンには個別のプライベートIPがあります。2台のマシンがどちらもポート3000を使えます(例:10.40.0.10:3000と10.40.0.11:3000)。接続後にCloudポートを右クリックし、「プライベートアドレスURLをコピー」を選びます。これらはプライベートアドレスで、公開共有リンクではありません。" + "value": "Es necesario actualizar el inicio de sesión, se muestra el último estado conocido" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "每台机器都有自己的私有 IP。两台机器可以同时使用端口 3000,例如 10.40.0.10:3000 和 10.40.0.11:3000。连接后右键点击 Cloud 端口,选择“复制私有地址 URL”。这些地址是私有地址,不是公开分享链接。" + "value": "La connexion doit être actualisée, affichage des dernières informations connues" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "每台機器都有自己的私有 IP。兩台機器可以同時使用連接埠 3000,例如 10.40.0.10:3000 和 10.40.0.11:3000。連線後對 Cloud 連接埠按右鍵,選擇「複製私有位址 URL」。這些位址是私有位址,不是公開分享連結。" + "value": "再サインインが必要です。最後の状態を表示中" } }, "ko": { "stringUnit": { "state": "translated", - "value": "각 머신에는 고유한 비공개 IP가 있습니다. 두 머신 모두 포트 3000을 사용할 수 있습니다(예: 10.40.0.10:3000 및 10.40.0.11:3000). 연결 후 Cloud 포트를 오른쪽 클릭하고 비공개 주소 URL 복사를 선택하세요. 이 주소는 비공개이며 공개 공유 링크가 아닙니다." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لكل جهاز عنوان IP خاص به. يمكن لجهازين استخدام المنفذ 3000، مثل 10.40.0.10:3000 و10.40.0.11:3000. بعد الاتصال، انقر بزر الماوس الأيمن على منفذ Cloud واختر نسخ رابط العنوان الخاص. هذه عناوين خاصة وليست روابط مشاركة عامة." + "value": "로그인 갱신 필요, 마지막으로 확인된 상태 표시 중" } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Svaka mašina ima svoju privatnu IP adresu. Dvije mašine mogu koristiti port 3000, na primjer 10.40.0.10:3000 i 10.40.0.11:3000. Nakon povezivanja kliknite desnim dugmetom na Cloud port i izaberite Kopiraj URL privatne adrese. Ovo su privatne adrese, a ne javne veze za dijeljenje." + "value": "登录状态需要刷新,显示的是最后已知状态" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Hver maskine har sin egen private IP. To maskiner kan begge bruge port 3000, f.eks. 10.40.0.10:3000 og 10.40.0.11:3000. Højreklik på en Cloud-port efter tilslutning, og vælg Kopiér URL til privat adresse. Adresserne er private, ikke offentlige delelinks." + "value": "登入狀態需要重新整理,顯示上次已知的狀態" } - }, - "it": { + } + } + }, + "cli.session.help": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Ogni macchina ha il proprio IP privato. Due macchine possono usare entrambe la porta 3000, ad esempio 10.40.0.10:3000 e 10.40.0.11:3000. Dopo la connessione fai clic destro su una porta Cloud e scegli Copia URL indirizzo privato. Sono indirizzi privati, non link pubblici." + "value": "Usage: cmux session restore [--list] [--session ]...\n\nReopen Claude sessions that were running when cmux last quit unexpectedly.\ncmux finds them in the agent journal, skips any that are running or already\nopen, and resumes each in its own workspace through the launcher that started\nit.\n\nWithout --session, restore acts only after an unexpected quit.\n\nOptions:\n --list Show the sessions without restoring them.\n --session Restore only this session (repeatable)." } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "ម៉ាស៊ីននីមួយៗមាន IP ឯកជនផ្ទាល់ខ្លួន។ ម៉ាស៊ីនពីរអាចប្រើច្រក 3000 ដូចគ្នា ឧទាហរណ៍ 10.40.0.10:3000 និង 10.40.0.11:3000។ បន្ទាប់ពីភ្ជាប់ ចុចខាងស្តាំលើច្រក Cloud ហើយជ្រើស ចម្លង URL អាសយដ្ឋានឯកជន។ ទាំងនេះជាអាសយដ្ឋានឯកជន មិនមែនតំណចែករំលែកសាធារណៈទេ។" + "value": "Verwendung: cmux session restore [--list] [--session ]...\n\nÖffnet Claude-Sitzungen erneut, die liefen, als cmux zuletzt unerwartet beendet wurde.\ncmux findet sie im Agent-Journal, überspringt laufende oder bereits geöffnete\nund setzt jede in einem eigenen Workspace\nüber den Launcher fort, der sie gestartet hat.\n\nOhne --session stellt restore nur nach einem unerwarteten Beenden wieder her.\n\nOptionen:\n --list Sitzungen anzeigen, ohne sie wiederherzustellen.\n --session Nur diese Sitzung wiederherstellen (wiederholbar)." } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Hver maskin har sin egen private IP. To maskiner kan bruke port 3000, for eksempel 10.40.0.10:3000 og 10.40.0.11:3000. Høyreklikk en Cloud-port etter tilkobling og velg Kopier URL for privat adresse. Adressene er private, ikke offentlige delingslenker." + "value": "Utilisation : cmux session restore [--list] [--session ]...\n\nRouvre les sessions Claude en cours lorsque cmux s’est fermé de façon inattendue.\ncmux les trouve dans le journal des agents, ignore celles qui tournent ou sont déjà\nouvertes, et reprend chacune dans son propre espace de travail via le lanceur qui l’a\ndémarrée.\n\nSans --session, restore n’agit qu’après une fermeture inattendue.\n\nOptions :\n --list Afficher les sessions sans les restaurer.\n --session Restaurer uniquement cette session (répétable)." } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Każda maszyna ma własny prywatny adres IP. Dwie maszyny mogą używać portu 3000, na przykład 10.40.0.10:3000 i 10.40.0.11:3000. Po połączeniu kliknij prawym przyciskiem port Cloud i wybierz Kopiuj URL prywatnego adresu. To prywatne adresy, nie publiczne linki udostępniania." + "value": "الاستخدام: cmux session restore [--list] [--session ]...\n\nيعيد فتح جلسات Claude التي كانت قيد التشغيل عندما أُغلق cmux بشكل غير متوقع آخر مرة.\nيعثر cmux عليها في سجل الوكيل، ويتخطى ما يعمل منها أو المفتوح بالفعل،\nويستأنف كل جلسة في مساحة عمل خاصة بها\nعبر المشغّل الذي بدأها.\n\nبدون --session، لا تعمل الاستعادة إلا بعد إغلاق غير متوقع.\n\nالخيارات:\n --list عرض الجلسات دون استعادتها.\n --session استعادة هذه الجلسة فقط (قابل للتكرار)." } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Cada máquina tem seu próprio IP privado. Duas máquinas podem usar a porta 3000, por exemplo 10.40.0.10:3000 e 10.40.0.11:3000. Depois de conectar, clique com o botão direito em uma porta Cloud e escolha Copiar URL do endereço privado. Esses endereços são privados, não links públicos de compartilhamento." + "value": "Uso: cmux session restore [--list] [--session ]...\n\nVuelve a abrir las sesiones de Claude que estaban en ejecución cuando cmux se cerró inesperadamente.\ncmux las encuentra en el diario de agentes, omite las que están en ejecución o ya\nabiertas, y reanuda cada una en su propio espacio de trabajo mediante el lanzador que\nla inició.\n\nSin --session, restore solo actúa tras un cierre inesperado.\n\nOpciones:\n --list Mostrar las sesiones sin restaurarlas.\n --session Restaurar solo esta sesión (repetible)." } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "У каждой машины свой частный IP. Две машины могут использовать порт 3000, например 10.40.0.10:3000 и 10.40.0.11:3000. После подключения щёлкните правой кнопкой мыши порт Cloud и выберите Скопировать URL частного адреса. Это частные адреса, а не публичные ссылки." + "value": "用法: cmux session restore [--list] [--session ]...\n\n重新開啟 cmux 上次意外結束時仍在執行的 Claude 工作階段。\ncmux 會從代理日誌中找出它們,略過仍在執行或已開啟的工作階段,\n並透過原本啟動它的啟動器\n在各自的工作區中繼續。\n\n未指定 --session 時,restore 只會在意外結束後執行。\n\n選項:\n --list 只列出工作階段,不還原。\n --session 只還原此工作階段(可重複)。" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "แต่ละเครื่องมี IP ส่วนตัวของตนเอง สองเครื่องสามารถใช้พอร์ต 3000 ได้ เช่น 10.40.0.10:3000 และ 10.40.0.11:3000 หลังเชื่อมต่อ คลิกขวาที่พอร์ต Cloud แล้วเลือกคัดลอก URL ที่อยู่ส่วนตัว ที่อยู่เหล่านี้เป็นส่วนตัว ไม่ใช่ลิงก์สาธารณะ" + "value": "用法: cmux session restore [--list] [--session ]...\n\n重新打开 cmux 上次意外退出时仍在运行的 Claude 会话。\ncmux 会从代理日志中找到它们,跳过仍在运行或已打开的会话,\n并通过最初启动它的启动器\n在各自的工作区中继续。\n\n未指定 --session 时,restore 只会在意外退出后执行。\n\n选项:\n --list 只列出会话,不恢复。\n --session 只恢复此会话(可重复)。" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Her makinenin kendi özel IP’si vardır. İki makine 3000 bağlantı noktasını kullanabilir; örneğin 10.40.0.10:3000 ve 10.40.0.11:3000. Bağlandıktan sonra bir Cloud portuna sağ tıklayın ve Özel Adres URL’sini Kopyala’yı seçin. Bunlar özel adreslerdir, herkese açık paylaşım bağlantıları değildir." + "value": "사용법: cmux session restore [--list] [--session ]...\n\ncmux가 마지막으로 예기치 않게 종료될 때 실행 중이던 Claude 세션을 다시 엽니다.\ncmux는 에이전트 저널에서 세션을 찾고, 실행 중이거나 이미 열려 있는 세션은 건너뛰며,\n각 세션을 시작한 런처를 통해\n자체 워크스페이스에서 이어갑니다.\n\n--session이 없으면 restore는 예기치 않은 종료 후에만 동작합니다.\n\n옵션:\n --list 복원하지 않고 세션만 표시합니다.\n --session 이 세션만 복원합니다(반복 가능)." } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Кожна машина має власну приватну IP-адресу. Дві машини можуть використовувати порт 3000, наприклад 10.40.0.10:3000 і 10.40.0.11:3000. Після підключення клацніть правою кнопкою миші порт Cloud і виберіть Копіювати URL приватної адреси. Це приватні адреси, а не публічні посилання для спільного доступу." + "value": "使用法: cmux session restore [--list] [--session ]...\n\ncmux が前回予期せず終了したときに実行中だった Claude セッションを再び開きます。\ncmux はエージェントジャーナルからセッションを見つけ、実行中または既に開いている\nものはスキップし、それぞれを起動したランチャー経由で\n専用のワークスペースで再開します。\n\n--session を指定しない場合、restore は予期しない終了の後にのみ動作します。\n\nオプション:\n --list 復元せずにセッションを表示します。\n --session このセッションのみ復元します(複数指定可)。" } } } }, - "cloud.vpn.setup.cancel": { + "cli.session.error.expectedRestore": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cancel" + "value": "session: expected 'restore' (see cmux help session)" } }, "de": { "stringUnit": { "state": "translated", - "value": "Abbrechen" + "value": "session: 'restore' erwartet (siehe cmux help session)" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Annuler" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Cancelar" + "value": "session : « restore » attendu (voir cmux help session)" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "キャンセル" + "value": "session: كان المتوقع 'restore' (راجع cmux help session)" } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "取消" + "value": "session: se esperaba 'restore' (consulta cmux help session)" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "取消" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "취소" + "value": "session:應為 'restore'(請參閱 cmux help session)" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "إلغاء" + "value": "session:应为 'restore'(请参阅 cmux help session)" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Otkaži" + "value": "session: 'restore'가 필요합니다(cmux help session 참조)" } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Annuller" + "value": "session: 'restore' が必要です(cmux help session を参照)" } - }, - "it": { + } + } + }, + "cli.session.error.sessionRequiresId": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Annulla" + "value": "session restore: --session requires an id" } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "បោះបង់" + "value": "session restore: --session erfordert eine ID" } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Avbryt" + "value": "session restore : --session nécessite un identifiant" } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Anuluj" + "value": "session restore: يتطلب --session معرّفًا" } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Cancelar" + "value": "session restore: --session requiere un id" } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Отменить" + "value": "session restore:--session 需要一個 ID" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ยกเลิก" + "value": "session restore:--session 需要一个 ID" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "İptal" + "value": "session restore: --session에는 ID가 필요합니다" } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Скасувати" + "value": "session restore: --session には ID が必要です" } } } }, - "cloud.vpn.setup.connect": { + "cli.session.error.unknownArgument": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Connect Cloud VPN" + "value": "session restore: unknown argument '%@'" } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-VPN verbinden" + "value": "session restore: unbekanntes Argument '%@'" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Connecter le VPN Cloud" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Conectar VPN de Cloud" + "value": "session restore : argument inconnu « %@ »" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "Cloud VPNに接続" + "value": "session restore: وسيط غير معروف '%@'" } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "连接 Cloud VPN" + "value": "session restore: argumento desconocido '%@'" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "連接 Cloud VPN" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Cloud VPN 연결" + "value": "session restore:未知的參數 '%@'" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "توصيل VPN الخاص بـ Cloud" + "value": "session restore:未知参数 '%@'" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Poveži Cloud VPN" + "value": "session restore: 알 수 없는 인수 '%@'" } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Forbind Cloud VPN" + "value": "session restore: 不明な引数 '%@'" } - }, - "it": { + } + } + }, + "cli.session.list.empty": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Connetti VPN Cloud" + "value": "No agent sessions to restore." } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "ភ្ជាប់ Cloud VPN" + "value": "Keine Agent-Sitzungen zum Wiederherstellen." } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Koble til Cloud VPN" + "value": "Aucune session d’agent à restaurer." } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Połącz z Cloud VPN" + "value": "لا توجد جلسات وكيل لاستعادتها." } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Conectar Cloud VPN" + "value": "No hay sesiones de agente para restaurar." } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Подключить Cloud VPN" + "value": "沒有可還原的代理工作階段。" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "เชื่อมต่อ Cloud VPN" + "value": "没有可恢复的代理会话。" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Cloud VPN’e bağlan" + "value": "복원할 에이전트 세션이 없습니다." } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Підключити Cloud VPN" + "value": "復元するエージェントセッションはありません。" } } } }, - "cloud.vpn.setup.connected": { + "cli.session.restore.cleanQuit": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Connected" + "value": "No agent sessions restored: cmux last quit normally. To restore one anyway, pass --session from --list." } }, "de": { "stringUnit": { "state": "translated", - "value": "Verbunden" + "value": "Keine Agent-Sitzungen wiederhergestellt: cmux wurde zuletzt normal beendet. Um trotzdem eine wiederherzustellen, übergib --session aus --list." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Connecté" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Conectada" + "value": "Aucune session d’agent restaurée : cmux s’est fermé normalement la dernière fois. Pour en restaurer une quand même, passez --session depuis --list." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "接続済み" + "value": "لم تتم استعادة أي جلسات وكيل: أُغلق cmux بشكل طبيعي آخر مرة. لاستعادة جلسة على أي حال، مرّر --session من --list." } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "已连接" + "value": "No se restauró ninguna sesión de agente: cmux se cerró normalmente la última vez. Para restaurar una de todos modos, pasa --session de --list." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已連線" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "연결됨" + "value": "未還原任何代理工作階段:cmux 上次是正常結束。若仍要還原,請傳入 --list 中的 --session 。" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "متصل" + "value": "未恢复任何代理会话:cmux 上次是正常退出。若仍要恢复,请传入 --list 中的 --session 。" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Povezano" + "value": "복원된 에이전트 세션이 없습니다. cmux가 마지막에 정상적으로 종료되었습니다. 그래도 복원하려면 --list에서 확인한 --session 를 전달하세요." } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Forbundet" + "value": "エージェントセッションは復元されませんでした。cmux は前回正常に終了しています。それでも復元する場合は、--list の --session を指定してください。" } - }, - "it": { + } + } + }, + "cli.session.restore.empty": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Connessa" + "value": "No agent sessions restored." } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "បានភ្ជាប់" + "value": "Keine Agent-Sitzungen wiederhergestellt." } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Tilkoblet" + "value": "Aucune session d’agent restaurée." } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Połączono" + "value": "لم تتم استعادة أي جلسات وكيل." } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Conectada" + "value": "No se restauró ninguna sesión de agente." } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Подключено" + "value": "未還原任何代理工作階段。" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "เชื่อมต่อแล้ว" + "value": "未恢复任何代理会话。" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Bağlandı" + "value": "복원된 에이전트 세션이 없습니다." } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Підключено" + "value": "エージェントセッションは復元されませんでした。" } } } }, - "cloud.vpn.setup.disconnect": { + "cli.session.restore.done": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Disconnect" + "value": "Agent sessions restored: %lld" } }, "de": { "stringUnit": { "state": "translated", - "value": "Trennen" + "value": "Wiederhergestellte Agent-Sitzungen: %lld" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Déconnecter" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Desconectar" + "value": "Sessions d’agent restaurées : %lld" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "切断" + "value": "جلسات الوكيل المستعادة: %lld" } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "断开连接" + "value": "Sesiones de agente restauradas: %lld" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "中斷連線" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "연결 해제" + "value": "已還原的代理工作階段:%lld" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "قطع الاتصال" + "value": "已恢复的代理会话:%lld" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Prekini vezu" + "value": "복원된 에이전트 세션: %lld" } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Afbryd" + "value": "復元したエージェントセッション: %lld" } - }, - "it": { + } + } + }, + "agentRecovery.alert.title": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Disconnetti" + "value": "Restore agent sessions?" } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "ផ្តាច់" + "value": "Agent-Sitzungen wiederherstellen?" } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Koble fra" + "value": "Restaurer les sessions d’agent ?" } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Rozłącz" + "value": "هل تريد استعادة جلسات الوكيل؟" } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Desconectar" + "value": "¿Restaurar las sesiones de agente?" } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Отключить" + "value": "要還原代理工作階段嗎?" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ตัดการเชื่อมต่อ" + "value": "要恢复代理会话吗?" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Bağlantıyı kes" + "value": "에이전트 세션을 복원할까요?" } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Від’єднати" + "value": "エージェントセッションを復元しますか?" } } } }, - "cloud.vpn.setup.entry.subtitle": { + "agentRecovery.alert.message": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Optional private IP access for other apps" + "value": "cmux quit unexpectedly while agent sessions were running. Sessions to restore: %lld. Each one reopens in its own workspace and resumes where it left off." } }, "de": { "stringUnit": { "state": "translated", - "value": "Optionaler Zugriff auf private IPs für andere Apps" + "value": "cmux wurde unerwartet beendet, während Agent-Sitzungen liefen. Wiederherzustellende Sitzungen: %lld. Jede wird in einem eigenen Workspace geöffnet und dort fortgesetzt, wo sie aufgehört hat." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Accès facultatif aux IP privées pour les autres apps" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Acceso opcional a IP privadas para otras apps" + "value": "cmux s’est fermé de façon inattendue pendant que des sessions d’agent étaient en cours. Sessions à restaurer : %lld. Chacune rouvre dans son propre espace de travail et reprend là où elle s’était arrêtée." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "ほかのアプリからプライベートIPへアクセスする任意の設定" + "value": "أُغلق cmux بشكل غير متوقع أثناء تشغيل جلسات الوكيل. الجلسات المطلوب استعادتها: %lld. تُفتح كل جلسة في مساحة عمل خاصة بها وتُستأنف من حيث توقفت." } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "为其他应用提供可选的私有 IP 访问" + "value": "cmux se cerró inesperadamente mientras había sesiones de agente en ejecución. Sesiones por restaurar: %lld. Cada una se abre en su propio espacio de trabajo y continúa donde se quedó." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "為其他 App 提供選用的私有 IP 存取" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "다른 앱의 비공개 IP 접근을 위한 선택 설정" + "value": "cmux 在代理工作階段執行時意外結束。要還原的工作階段:%lld。每個工作階段都會在各自的工作區重新開啟,並從中斷處繼續。" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "وصول اختياري إلى عناوين IP الخاصة للتطبيقات الأخرى" + "value": "cmux 在代理会话运行时意外退出。要恢复的会话:%lld。每个会话都会在各自的工作区重新打开,并从中断处继续。" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Opcionalni pristup privatnim IP adresama za druge aplikacije" + "value": "에이전트 세션이 실행 중일 때 cmux가 예기치 않게 종료되었습니다. 복원할 세션: %lld. 각 세션은 자체 워크스페이스에서 다시 열리고 중단된 곳에서 이어집니다." } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Valgfri privat IP-adgang for andre apps" + "value": "エージェントセッションの実行中に cmux が予期せず終了しました。復元するセッション: %lld。各セッションは専用のワークスペースで再び開き、中断したところから再開します。" } - }, - "it": { + } + } + }, + "agentRecovery.alert.restore": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Accesso facoltativo agli IP privati per altre app" + "value": "Restore Agent Sessions" } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "ចូលប្រើ IP ឯកជនសម្រាប់កម្មវិធីផ្សេងៗជាជម្រើស" + "value": "Agent-Sitzungen wiederherstellen" } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Valgfri privat IP-tilgang for andre apper" + "value": "Restaurer les sessions d’agent" } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Opcjonalny dostęp innych aplikacji do prywatnych adresów IP" + "value": "استعادة جلسات الوكيل" } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Acesso opcional a IPs privados para outros apps" + "value": "Restaurar sesiones de agente" } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Необязательный доступ других приложений к частным IP" + "value": "還原代理工作階段" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "การเข้าถึง IP ส่วนตัวสำหรับแอปอื่นแบบไม่บังคับ" + "value": "恢复代理会话" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Diğer uygulamalar için isteğe bağlı özel IP erişimi" + "value": "에이전트 세션 복원" } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Необов’язковий доступ інших програм до приватних IP" + "value": "エージェントセッションを復元" } } } }, - "cloud.vpn.setup.failed": { + "agentRecovery.alert.notNow": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Needs attention" + "value": "Not Now" } }, "de": { "stringUnit": { "state": "translated", - "value": "Aktion erforderlich" + "value": "Nicht jetzt" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Action requise" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Requiere atención" + "value": "Pas maintenant" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "対応が必要" + "value": "ليس الآن" } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "需要处理" + "value": "Ahora no" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需要處理" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "조치 필요" + "value": "稍後" } }, - "ar": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "يتطلب إجراءً" + "value": "以后再说" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Potrebna pažnja" + "value": "나중에" } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "Kræver opmærksomhed" + "value": "後で" } - }, - "it": { + } + } + }, + "settings.notifications.soundWhenFocused.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Richiede attenzione" + "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." } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "ត្រូវការការយកចិត្តទុកដាក់" + "value": "شغّل صوت الإشعار حتى عندما تكون اللوحة التي أرسلت الإشعار في موضع التركيز بالفعل. عند إيقافه، تعرض اللوحة المُركَّز عليها حلقتها دون صوت." } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Krever handling" + "value": "Den Benachrichtigungston auch abspielen, wenn der benachrichtigende Bereich bereits fokussiert ist. Wenn aus, zeigt ein fokussierter Bereich seinen Ring ohne Ton." } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Wymaga uwagi" + "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." } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Requer atenção" + "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." } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Требуется действие" + "value": "通知を送ったペインがすでにフォーカスされている場合でも通知音を鳴らします。オフの場合、フォーカス中のペインは音を鳴らさずにリングを表示します。" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "ต้องดำเนินการ" + "value": "알림을 보낸 패널이 이미 포커스된 상태여도 알림음을 재생합니다. 끄면 포커스된 패널은 소리 없이 링만 표시합니다." } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "İlgilenilmeli" + "value": "即使发出通知的窗格已处于焦点状态,也播放通知声音。关闭时,处于焦点的窗格会显示光环但不发出声音。" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Потрібна увага" + "value": "即使發出通知的窗格已在焦點中,也播放通知聲音。關閉時,焦點中的窗格會顯示光環但不發出聲音。" } } } }, - "cloud.vpn.setup.off": { + "settings.notifications.soundWhenFocused.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Off" + "value": "Sound for Focused Pane" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Aus" + "value": "صوت للوحة المُركَّز عليها" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "Désactivé" + "value": "Ton für fokussierten Bereich" } }, "es": { "stringUnit": { "state": "translated", - "value": "Desactivada" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "オフ" + "value": "Sonido para el panel con foco" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "已关闭" + "value": "Son pour le panneau actif" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "已關閉" + "value": "フォーカス中のペインの通知音" } }, "ko": { "stringUnit": { "state": "translated", - "value": "꺼짐" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "متوقف" + "value": "포커스된 패널 소리" } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Isključeno" + "value": "焦点窗格的声音" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Fra" + "value": "焦點窗格的聲音" } - }, - "it": { + } + } + }, + "agent.claude.stopFailure.body.generic": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Disattivata" + "value": "Claude Code stopped on an API error" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "បិទ" + "value": "Claude Code は API エラーで停止しました" } }, - "nb": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Av" + "value": "Claude Code 因 API 错误而停止" } }, - "pl": { + "de": { "stringUnit": { "state": "translated", - "value": "Wyłączona" + "value": "Claude Code wurde wegen eines API-Fehlers gestoppt" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Desativada" + "value": "Claude Code s'est arrêté sur une erreur d'API" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "Выключено" + "value": "توقف Claude Code بسبب خطأ في واجهة API" } }, - "th": { + "es": { "stringUnit": { "state": "translated", - "value": "ปิด" + "value": "Claude Code se detuvo por un error de la API" } }, - "tr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Kapalı" + "value": "Claude Code 因 API 錯誤而停止" } }, - "uk": { + "ko": { "stringUnit": { "state": "translated", - "value": "Вимкнено" + "value": "Claude Code가 API 오류로 중지되었습니다" } } } }, - "cloud.vpn.setup.openUnavailable": { + "agent.claude.stopFailure.status.apiError": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud VPN setup is unavailable" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Cloud-VPN-Einrichtung ist nicht verfügbar" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "La configuration du VPN Cloud est indisponible" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "La configuración de VPN de Cloud no está disponible" + "value": "API error" } }, "ja": { "stringUnit": { "state": "translated", - "value": "Cloud VPN設定は利用できません" + "value": "API エラー" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud VPN 设置不可用" + "value": "API 错误" } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "無法使用 Cloud VPN 設定" + "value": "API-Fehler" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "Cloud VPN 설정을 사용할 수 없습니다" + "value": "Erreur d'API" } }, "ar": { "stringUnit": { "state": "translated", - "value": "إعداد VPN الخاص بـ Cloud غير متاح" + "value": "خطأ في واجهة API" } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "Podešavanje Cloud VPN-a nije dostupno" + "value": "Error de la API" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud VPN-opsætning er ikke tilgængelig" + "value": "API 錯誤" } }, - "it": { + "ko": { "stringUnit": { "state": "translated", - "value": "La configurazione VPN Cloud non è disponibile" + "value": "API 오류" + } + } + } + }, + "agent.claude.stopFailure.status.authentication": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Auth error" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "ការរៀបចំ Cloud VPN មិនអាចប្រើបាន" + "value": "認証エラー" } }, - "nb": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud VPN-oppsett er ikke tilgjengelig" + "value": "认证错误" } }, - "pl": { + "de": { "stringUnit": { "state": "translated", - "value": "Konfiguracja Cloud VPN jest niedostępna" + "value": "Authentifizierungsfehler" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "A configuração do Cloud VPN não está disponível" + "value": "Erreur d'authentification" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "Настройка Cloud VPN недоступна" + "value": "خطأ في المصادقة" } }, - "th": { + "es": { "stringUnit": { "state": "translated", - "value": "การตั้งค่า Cloud VPN ไม่พร้อมใช้งาน" + "value": "Error de autenticación" } }, - "tr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud VPN kurulumu kullanılamıyor" + "value": "驗證錯誤" } }, - "uk": { + "ko": { "stringUnit": { "state": "translated", - "value": "Налаштування Cloud VPN недоступне" + "value": "인증 오류" } } } }, - "cloud.vpn.setup.permission.body": { + "agent.claude.stopFailure.status.billing": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The VPN software is included in cmux. You do not need another app. macOS may require your password or Touch ID during approval." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die VPN-Software ist in cmux enthalten. Sie brauchen keine weitere App. macOS kann bei der Genehmigung Ihr Passwort oder Touch ID verlangen." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Le logiciel VPN est inclus dans cmux. Aucune autre app n’est nécessaire. macOS peut demander votre mot de passe ou Touch ID lors de l’autorisation." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "El software VPN está incluido en cmux. No necesitas otra app. macOS puede solicitar tu contraseña o Touch ID durante la autorización." + "value": "Billing error" } }, "ja": { "stringUnit": { "state": "translated", - "value": "VPNソフトウェアはcmuxに含まれています。別のアプリは不要です。承認時にmacOSがパスワードまたはTouch IDを求めることがあります。" + "value": "請求エラー" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 已包含 VPN 软件,无需其他应用。批准时,macOS 可能要求您输入密码或使用触控 ID。" + "value": "账单错误" } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "cmux 已包含 VPN 軟體,不需要其他 App。核准時,macOS 可能要求您輸入密碼或使用 Touch ID。" + "value": "Abrechnungsfehler" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "VPN 소프트웨어는 cmux에 포함되어 있습니다. 다른 앱은 필요하지 않습니다. 승인 시 macOS에서 암호 또는 Touch ID를 요청할 수 있습니다." + "value": "Erreur de facturation" } }, "ar": { "stringUnit": { "state": "translated", - "value": "برنامج VPN مضمن في cmux. لا تحتاج إلى تطبيق آخر. قد يطلب macOS كلمة المرور أو Touch ID أثناء الموافقة." + "value": "خطأ في الفوترة" } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "VPN softver je uključen u cmux. Nije potrebna druga aplikacija. macOS može zatražiti lozinku ili Touch ID tokom odobravanja." + "value": "Error de facturación" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "VPN-softwaren er inkluderet i cmux. Du behøver ingen anden app. macOS kan bede om din adgangskode eller Touch ID under godkendelsen." + "value": "帳單錯誤" } }, - "it": { + "ko": { "stringUnit": { "state": "translated", - "value": "Il software VPN è incluso in cmux. Non serve un’altra app. macOS potrebbe chiedere la password o Touch ID durante l’autorizzazione." + "value": "결제 오류" + } + } + } + }, + "agent.claude.stopFailure.status.connectionDropped": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connection dropped" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "កម្មវិធី VPN មាននៅក្នុង cmux។ អ្នកមិនត្រូវការកម្មវិធីផ្សេងទៀតទេ។ macOS អាចស្នើពាក្យសម្ងាត់ ឬ Touch ID ក្នុងពេលអនុម័ត។" + "value": "接続が切断されました" } }, - "nb": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "VPN-programvaren er inkludert i cmux. Du trenger ingen annen app. macOS kan be om passord eller Touch ID under godkjenningen." + "value": "连接已断开" } }, - "pl": { + "de": { "stringUnit": { "state": "translated", - "value": "Oprogramowanie VPN jest zawarte w cmux. Nie potrzebujesz innej aplikacji. macOS może poprosić o hasło lub Touch ID podczas zatwierdzania." + "value": "Verbindung abgebrochen" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "O software VPN vem incluído no cmux. Você não precisa de outro app. O macOS pode pedir sua senha ou Touch ID durante a aprovação." + "value": "Connexion interrompue" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "VPN-программа уже входит в cmux. Другие приложения не нужны. Во время подтверждения macOS может запросить пароль или Touch ID." + "value": "انقطع الاتصال" } }, - "th": { + "es": { "stringUnit": { "state": "translated", - "value": "ซอฟต์แวร์ VPN รวมอยู่ใน cmux แล้ว ไม่ต้องใช้แอปอื่น macOS อาจขอรหัสผ่านหรือ Touch ID ระหว่างการอนุมัติ" + "value": "Conexión perdida" } }, - "tr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "VPN yazılımı cmux içinde bulunur. Başka bir uygulama gerekmez. macOS onay sırasında parolanızı veya Touch ID’yi isteyebilir." + "value": "連線已中斷" } }, - "uk": { + "ko": { "stringUnit": { "state": "translated", - "value": "Програмне забезпечення VPN входить до cmux. Інша програма не потрібна. Під час схвалення macOS може попросити пароль або Touch ID." + "value": "연결 끊김" } } } }, - "cloud.vpn.setup.status.title": { + "agent.claude.stopFailure.status.outputLimit": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Private network status" + "value": "Output limit" } }, - "de": { + "ja": { "stringUnit": { "state": "translated", - "value": "Status des privaten Netzwerks" + "value": "出力上限" } }, - "fr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "État du réseau privé" + "value": "输出上限" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Estado de la red privada" + "value": "Ausgabelimit" } }, - "ja": { + "fr": { "stringUnit": { "state": "translated", - "value": "プライベートネットワークの状態" + "value": "Limite de sortie" } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "私有网络状态" + "value": "حد المخرجات" } }, - "zh-Hant": { + "es": { "stringUnit": { "state": "translated", - "value": "私有網路狀態" + "value": "Límite de salida" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "비공개 네트워크 상태" + "value": "輸出上限" } }, - "ar": { + "ko": { "stringUnit": { "state": "translated", - "value": "حالة الشبكة الخاصة" + "value": "출력 한도" } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Status privatne mreže" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Status for privat netværk" - } - }, - "it": { + } + } + }, + "agent.claude.stopFailure.status.overloaded": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Stato della rete privata" + "value": "API overloaded" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "ស្ថានភាពបណ្តាញឯកជន" + "value": "API 過負荷" } }, - "nb": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Status for privat nettverk" + "value": "API 过载" } }, - "pl": { + "de": { "stringUnit": { "state": "translated", - "value": "Stan sieci prywatnej" + "value": "API überlastet" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Status da rede privada" + "value": "API surchargée" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "Состояние частной сети" + "value": "واجهة API محمّلة فوق طاقتها" } }, - "th": { + "es": { "stringUnit": { "state": "translated", - "value": "สถานะเครือข่ายส่วนตัว" + "value": "API sobrecargada" } }, - "tr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Özel ağ durumu" + "value": "API 過載" } }, - "uk": { + "ko": { "stringUnit": { "state": "translated", - "value": "Стан приватної мережі" + "value": "API 과부하" } } } }, - "cloud.vpn.setup.title": { + "agent.claude.stopFailure.status.rateLimited": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Cloud-VPN" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "VPN Cloud" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "VPN de Cloud" + "value": "Rate limited" } }, "ja": { "stringUnit": { "state": "translated", - "value": "クラウドVPN" + "value": "レート制限中" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "云端 VPN" + "value": "已限速" } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "雲端 VPN" + "value": "Ratenbegrenzt" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "클라우드 VPN" + "value": "Limite de requêtes atteinte" } }, "ar": { "stringUnit": { "state": "translated", - "value": "شبكة Cloud الخاصة" + "value": "تم تقييد المعدل" } }, - "bs": { + "es": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "Límite de frecuencia alcanzado" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "已限速" } }, - "it": { + "ko": { "stringUnit": { "state": "translated", - "value": "VPN Cloud" + "value": "속도 제한됨" + } + } + } + }, + "agent.claude.stopFailure.status.usageLimit": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Usage limit" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "使用量の上限" } }, - "nb": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "用量上限" } }, - "pl": { + "de": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "Nutzungslimit" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "Limite d'utilisation" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "حد الاستخدام" } }, - "th": { + "es": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "Límite de uso" } }, - "tr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "用量上限" } }, - "uk": { + "ko": { "stringUnit": { "state": "translated", - "value": "Cloud VPN" + "value": "사용량 한도" } } } }, - "cloud.vpn.setup.unavailable": { + "agent.claude.stopFailure.status.usageLimitResets": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This copy of cmux does not include a signed VPN extension. Use a cmux release with Cloud VPN support. Cloud terminals, Ports, and Desktop remain available without the VPN." + "value": "Usage limit, resets %@" } }, - "de": { + "ja": { "stringUnit": { "state": "translated", - "value": "Diese cmux-Version enthält keine signierte VPN-Erweiterung. Verwenden Sie eine cmux-Version mit Cloud-VPN-Unterstützung. Cloud-Terminals, Ports und Desktop bleiben ohne VPN verfügbar." + "value": "使用量の上限、%@ にリセット" } }, - "fr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cette copie de cmux ne contient pas d’extension VPN signée. Utilisez une version de cmux qui prend en charge le VPN Cloud. Les terminaux Cloud, Ports et Bureau restent disponibles sans VPN." + "value": "用量上限,%@ 重置" } }, - "es": { + "de": { "stringUnit": { "state": "translated", - "value": "Esta copia de cmux no incluye una extensión VPN firmada. Usa una versión de cmux compatible con VPN de Cloud. Los terminales Cloud, Puertos y Escritorio siguen disponibles sin VPN." + "value": "Nutzungslimit, Reset %@" } }, - "ja": { + "fr": { "stringUnit": { "state": "translated", - "value": "このcmuxには署名済みのVPN機能拡張が含まれていません。Cloud VPN対応のcmuxリリースを使用してください。Cloudのターミナル、ポート、デスクトップはVPNなしでも使えます。" + "value": "Limite d'utilisation, réinitialisation %@" } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "此 cmux 副本未包含已签名的 VPN 扩展。请使用支持 Cloud VPN 的 cmux 版本。Cloud 终端、端口和桌面无需 VPN 仍可使用。" + "value": "حد الاستخدام، يُعاد التعيين %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Límite de uso, se restablece %@" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此 cmux 副本未包含已簽署的 VPN 延伸功能。請使用支援 Cloud VPN 的 cmux 版本。Cloud 終端機、連接埠和桌面不需要 VPN 仍可使用。" + "value": "用量上限,%@ 重設" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 cmux에는 서명된 VPN 확장이 포함되어 있지 않습니다. Cloud VPN을 지원하는 cmux 릴리스를 사용하세요. Cloud 터미널, 포트 및 데스크탑은 VPN 없이도 사용할 수 있습니다." + "value": "사용량 한도, %@에 초기화" } - }, + } + } + }, + "settings.app.reorderOnNotification.mode.agentActivity": { + "extractionState": "manual", + "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "لا تتضمن هذه النسخة من cmux امتداد VPN موقعًا. استخدم إصدار cmux يدعم VPN الخاص بـ Cloud. تظل طرفيات Cloud والمنافذ وسطح المكتب متاحة بدون VPN." + "value": "نشاط الوكيل" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Ova kopija cmux-a nema potpisani VPN dodatak. Koristite cmux izdanje sa Cloud VPN podrškom. Cloud terminali, portovi i radna površina ostaju dostupni bez VPN-a." + "value": "Aktivnost agenata" } }, "da": { "stringUnit": { "state": "translated", - "value": "Denne cmux-kopi indeholder ikke en signeret VPN-udvidelse. Brug en cmux-udgave med Cloud VPN-understøttelse. Cloud-terminaler, porte og skrivebord er stadig tilgængelige uden VPN." + "value": "Agentaktivitet" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agentenaktivität" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Agent Activity" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actividad de agentes" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activité des agents" } }, "it": { "stringUnit": { "state": "translated", - "value": "Questa copia di cmux non include un’estensione VPN firmata. Usa una versione di cmux con supporto Cloud VPN. Terminali Cloud, porte e desktop restano disponibili senza VPN." + "value": "Attività degli agenti" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "cmux ច្បាប់នេះមិនមានផ្នែកបន្ថែម VPN ដែលបានចុះហត្ថលេខាទេ។ ប្រើកំណែ cmux ដែលគាំទ្រ Cloud VPN។ Terminal, Ports និង Desktop របស់ Cloud នៅតែប្រើបានដោយគ្មាន VPN។" + "value": "エージェントのアクティビティ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트 활동" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Denne cmux-kopien har ingen signert VPN-utvidelse. Bruk en cmux-versjon med Cloud VPN-støtte. Cloud-terminaler, porter og skrivebord er tilgjengelige uten VPN." + "value": "Agentaktivitet" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Ta kopia cmux nie zawiera podpisanego rozszerzenia VPN. Użyj wydania cmux z obsługą Cloud VPN. Terminale Cloud, porty i pulpit pozostają dostępne bez VPN." + "value": "Aktywność agentów" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Esta cópia do cmux não inclui uma extensão VPN assinada. Use uma versão do cmux com suporte a Cloud VPN. Terminais Cloud, portas e área de trabalho continuam disponíveis sem VPN." + "value": "Atividade dos agentes" } }, "ru": { "stringUnit": { "state": "translated", - "value": "В этой копии cmux нет подписанного VPN-расширения. Используйте выпуск cmux с поддержкой Cloud VPN. Cloud-терминалы, порты и рабочий стол доступны без VPN." + "value": "Активность агентов" } }, "th": { "stringUnit": { "state": "translated", - "value": "cmux รุ่นนี้ไม่มีส่วนขยาย VPN ที่ลงนามแล้ว ใช้รุ่น cmux ที่รองรับ Cloud VPN เทอร์มินัล Cloud พอร์ต และเดสก์ท็อปยังใช้ได้โดยไม่ต้องมี VPN" + "value": "กิจกรรมของเอเจนต์" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu cmux kopyasında imzalı VPN uzantısı bulunmuyor. Cloud VPN destekli bir cmux sürümü kullanın. Cloud terminalleri, portlar ve masaüstü VPN olmadan da kullanılabilir." + "value": "Ajan etkinliği" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Ця копія cmux не містить підписаного VPN-розширення. Використовуйте випуск cmux із підтримкою Cloud VPN. Cloud-термінали, порти й робочий стіл доступні без VPN." - } - } - } - }, - "cloud.vpn.setup.waiting": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Waiting" + "value": "Активність агентів" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Warten" + "value": "代理活动" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "En attente" + "value": "代理活動" } - }, - "es": { + } + } + }, + "settings.app.reorderOnNotification.mode.notifications": { + "extractionState": "manual", + "localizations": { + "ar": { "stringUnit": { "state": "translated", - "value": "En espera" + "value": "الإشعارات" } }, - "ja": { + "bs": { "stringUnit": { "state": "translated", - "value": "待機中" + "value": "Obavještenja" } }, - "zh-Hans": { + "da": { "stringUnit": { "state": "translated", - "value": "等待中" + "value": "Notifikationer" } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "等待中" + "value": "Benachrichtigungen" } }, - "ko": { + "en": { "stringUnit": { "state": "translated", - "value": "대기 중" + "value": "Notifications" } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "قيد الانتظار" + "value": "Notificaciones" } }, - "bs": { + "fr": { "stringUnit": { "state": "translated", - "value": "Čekanje" + "value": "Sur notification" } }, - "da": { + "it": { "stringUnit": { "state": "translated", - "value": "Venter" + "value": "Notifiche" } }, - "it": { + "ja": { "stringUnit": { "state": "translated", - "value": "In attesa" + "value": "通知" } }, - "km": { + "ko": { "stringUnit": { "state": "translated", - "value": "កំពុងរង់ចាំ" + "value": "알림" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Venter" + "value": "Varsler" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Oczekiwanie" + "value": "Powiadomienia" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Aguardando" + "value": "Notificações" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Ожидание" + "value": "Уведомления" } }, "th": { "stringUnit": { "state": "translated", - "value": "กำลังรอ" + "value": "การแจ้งเตือน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bekliyor" + "value": "Bildirimler" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Очікування" - } - } - } - }, - "cloudTree.ports.action.wake": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Wake Machine" + "value": "Сповіщення" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Maschine aktivieren" + "value": "通知" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Réveiller la machine" + "value": "通知" } - }, + } + } + }, + "settings.app.reorderOnNotification.mode.off": { + "extractionState": "manual", + "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "إيقاظ الجهاز" + "value": "إيقاف" } }, - "es": { + "bs": { "stringUnit": { "state": "translated", - "value": "Activar máquina" + "value": "Isključeno" } }, - "zh-Hant": { + "da": { "stringUnit": { "state": "translated", - "value": "喚醒機器" + "value": "Fra" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "唤醒机器" + "value": "Aus" } }, - "ko": { + "en": { "stringUnit": { "state": "translated", - "value": "머신 깨우기" + "value": "Off" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "マシンを起動" + "value": "Desactivado" } }, - "bs": { + "fr": { "stringUnit": { "state": "translated", - "value": "Probudi mašinu" + "value": "Désactivé" } }, - "da": { + "it": { "stringUnit": { "state": "translated", - "value": "Væk maskine" + "value": "Disattivato" } }, - "it": { + "ja": { "stringUnit": { "state": "translated", - "value": "Riattiva macchina" + "value": "オフ" } }, - "km": { + "ko": { "stringUnit": { "state": "translated", - "value": "ដាស់ម៉ាស៊ីន" + "value": "끔" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Vekk maskin" + "value": "Av" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Wybudź maszynę" + "value": "Wyłączone" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Despertar máquina" + "value": "Desativado" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Разбудить машину" + "value": "Выкл." } }, "th": { "stringUnit": { "state": "translated", - "value": "ปลุกเครื่อง" + "value": "ปิด" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Makineyi uyandır" + "value": "Kapalı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Розбудити машину" - } - } - } - }, - "cloudTree.ports.asleep.detail": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Wake the machine to check its services. cmux’s in-app forwarding does not need Cloud VPN." + "value": "Вимкнено" } }, - "de": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Aktivieren Sie die Maschine, um ihre Dienste zu prüfen. Die Weiterleitung in cmux benötigt kein Cloud-VPN." + "value": "关闭" } }, - "fr": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Réveillez la machine pour vérifier ses services. Le transfert intégré à cmux ne nécessite pas de VPN Cloud." + "value": "關閉" } - }, + } + } + }, + "settings.app.reorderOnNotification.modeSubtitle": { + "extractionState": "manual", + "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "أيقظ الجهاز لفحص خدماته. لا تحتاج إعادة التوجيه داخل cmux إلى Cloud VPN." + "value": "ينقل مساحات العمل إلى الأعلى عندما تتلقى إشعارًا. مع نشاط الوكيل، ينقلها أيضًا عندما ترسل مطالبة أو ينهي وكيل دورة أو يحتاج إلى إدخال أو يفشل، لكن ليس أبدًا أثناء وجود المؤشر فوق الشريط الجانبي. الإيقاف يحافظ على ثبات مواضع الاختصارات." } }, - "es": { + "bs": { "stringUnit": { "state": "translated", - "value": "Activa la máquina para comprobar sus servicios. El reenvío integrado de cmux no necesita Cloud VPN." + "value": "Pomjera radne prostore na vrh kada prime obavještenje. Aktivnost agenata ih pomjera i kada pošaljete upit ili agent završi potez, treba unos ili ne uspije, ali nikad dok je pokazivač iznad bočne trake. Isključeno čuva stabilne pozicije prečica." } }, - "zh-Hant": { + "da": { "stringUnit": { "state": "translated", - "value": "喚醒機器以檢查其服務。cmux 的應用程式內轉送不需要 Cloud VPN。" + "value": "Flytter arbejdsområder til toppen, når de modtager en notifikation. Agentaktivitet flytter dem også, når du sender en prompt eller en agent afslutter en tur, har brug for input eller fejler, men aldrig mens markøren er over sidepanelet. Fra holder genvejspositionerne stabile." } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "唤醒机器以检查其服务。cmux 的应用内转发无需 Cloud VPN。" + "value": "Verschiebt Workspaces nach oben, wenn sie eine Benachrichtigung erhalten. Mit Agentenaktivität auch dann, wenn Sie einen Prompt senden oder ein Agent einen Durchlauf beendet, eine Eingabe braucht oder fehlschlägt, aber nie, während der Zeiger über der Seitenleiste ist. Aus hält die Positionen der Tastenkürzel stabil." } }, - "ko": { + "en": { "stringUnit": { "state": "translated", - "value": "머신을 깨워 서비스를 확인하세요. cmux 앱 내 전달에는 Cloud VPN이 필요하지 않습니다." + "value": "Move workspaces to the top when they receive a notification. Agent Activity also moves them when you send a prompt or an agent finishes a turn, needs input, or fails, but never while you point at the sidebar. Off keeps shortcut positions stable." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "マシンを起動してサービスを確認してください。cmux のアプリ内転送に Cloud VPN は不要です。" + "value": "Mueve los espacios de trabajo arriba cuando reciben una notificación. Con Actividad de agentes, también cuando envías un prompt o un agente termina un turno, necesita una respuesta o falla, pero nunca mientras el puntero está sobre la barra lateral. Desactivado mantiene estables las posiciones de los atajos." } }, - "bs": { + "fr": { "stringUnit": { "state": "translated", - "value": "Probudite mašinu da provjerite njene servise. Prosljeđivanje unutar cmux-a ne treba Cloud VPN." + "value": "Place les espaces de travail en haut lorsqu’ils reçoivent une notification. Avec Activité des agents, aussi lorsque vous envoyez un prompt ou qu’un agent termine un tour, attend une saisie ou échoue, mais jamais pendant que le pointeur survole la barre latérale. Désactivé garde les positions des raccourcis stables." } }, - "da": { + "it": { "stringUnit": { "state": "translated", - "value": "Væk maskinen for at kontrollere dens tjenester. Videresendelse i cmux kræver ikke Cloud VPN." + "value": "Sposta gli spazi di lavoro in cima quando ricevono una notifica. Attività degli agenti li sposta anche quando invii un prompt o un agente termina un turno, attende un input o fallisce, ma mai mentre il puntatore è sulla barra laterale. Disattivato mantiene stabili le posizioni delle scorciatoie." } }, - "it": { + "ja": { "stringUnit": { "state": "translated", - "value": "Riattiva la macchina per controllarne i servizi. L’inoltro all’interno di cmux non richiede la VPN Cloud." + "value": "通知を受け取ったワークスペースを先頭に移動します。「エージェントのアクティビティ」では、プロンプトを送信したときや、エージェントがターンを終えたとき、入力を待っているとき、失敗したときにも移動しますが、ポインタがサイドバー上にある間は移動しません。「オフ」ではショートカットの位置が変わりません。" } }, - "km": { + "ko": { "stringUnit": { "state": "translated", - "value": "ដាស់ម៉ាស៊ីនដើម្បីពិនិត្យសេវាកម្មរបស់វា។ ការបញ្ជូនបន្តនៅក្នុង cmux មិនត្រូវការ Cloud VPN ទេ។" + "value": "알림을 받으면 워크스페이스를 맨 위로 옮깁니다. 에이전트 활동을 선택하면 프롬프트를 보내거나 에이전트가 턴을 마치거나 입력이 필요하거나 실패할 때도 옮기지만, 포인터가 사이드바 위에 있을 때는 옮기지 않습니다. 끔은 단축키 위치를 고정합니다." } }, "nb": { "stringUnit": { "state": "translated", - "value": "Vekk maskinen for å sjekke tjenestene. Videresending i cmux trenger ikke Cloud VPN." + "value": "Flytter arbeidsområder til toppen når de mottar et varsel. Agentaktivitet flytter dem også når du sender en ledetekst eller en agent fullfører en runde, trenger input eller feiler, men aldri mens pekeren er over sidefeltet. Av holder snarveisposisjonene stabile." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Wybudź maszynę, aby sprawdzić jej usługi. Przekierowywanie w cmux nie wymaga Cloud VPN." + "value": "Przenosi obszary robocze na górę, gdy otrzymają powiadomienie. Aktywność agentów przenosi je także, gdy wyślesz prompt lub agent zakończy turę, potrzebuje danych lub zakończy się błędem, ale nigdy, gdy wskaźnik jest nad paskiem bocznym. Wyłączone zachowuje stałe pozycje skrótów." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Desperte a máquina para verificar os serviços. O encaminhamento dentro do cmux não precisa do Cloud VPN." + "value": "Move os espaços de trabalho para o topo quando recebem uma notificação. Atividade dos agentes também os move quando você envia um prompt ou um agente termina um turno, precisa de uma resposta ou falha, mas nunca enquanto o ponteiro está sobre a barra lateral. Desativado mantém as posições dos atalhos estáveis." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Разбудите машину, чтобы проверить её службы. Перенаправлению внутри cmux Cloud VPN не нужен." + "value": "Перемещает рабочие области наверх, когда они получают уведомление. «Активность агентов» также перемещает их, когда вы отправляете запрос или агент завершает ход, ждёт ввода или завершается с ошибкой, но никогда, пока указатель находится над боковой панелью. «Выкл.» сохраняет позиции сочетаний клавиш." } }, "th": { "stringUnit": { "state": "translated", - "value": "ปลุกเครื่องเพื่อตรวจสอบบริการ การส่งต่อภายใน cmux ไม่ต้องใช้ Cloud VPN" + "value": "ย้ายเวิร์กสเปซขึ้นด้านบนเมื่อได้รับการแจ้งเตือน กิจกรรมของเอเจนต์จะย้ายเมื่อคุณส่งพรอมต์หรือเอเจนต์จบรอบ ต้องการข้อมูล หรือล้มเหลวด้วย แต่จะไม่ย้ายขณะที่ตัวชี้อยู่เหนือแถบด้านข้าง ปิดจะคงตำแหน่งของทางลัดไว้" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Hizmetlerini denetlemek için makineyi uyandırın. cmux içi yönlendirme Cloud VPN gerektirmez." + "value": "Bildirim aldıklarında çalışma alanlarını en üste taşır. Ajan etkinliği, bir istem gönderdiğinizde, bir ajan turunu bitirdiğinde, girdi beklediğinde veya başarısız olduğunda da taşır, ancak işaretçi kenar çubuğunun üzerindeyken asla taşımaz. Kapalı, kısayol konumlarını sabit tutar." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Розбудіть машину, щоб перевірити її служби. Перенаправлення всередині cmux не потребує Cloud VPN." + "value": "Переміщує робочі області вгору, коли вони отримують сповіщення. «Активність агентів» також переміщує їх, коли ви надсилаєте запит або агент завершує хід, чекає на введення або завершується з помилкою, але ніколи, поки вказівник над бічною панеллю. «Вимкнено» зберігає позиції скорочень." } - } - } - }, - "cloudTree.ports.available.title": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Listening services" + "value": "工作区收到通知时移到顶部。选择“代理活动”时,你发送提示词或代理完成一轮、需要输入或失败时也会移动,但指针停在侧边栏上时绝不移动。关闭可让快捷键位置保持不变。" } }, - "de": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Lauschende Dienste" + "value": "工作區收到通知時移到頂部。選擇「代理活動」時,你送出提示詞或代理完成一輪、需要輸入或失敗時也會移動,但指標停在側邊欄上時絕不移動。關閉可讓快捷鍵位置保持不變。" } - }, - "fr": { + } + } + }, + "cli.restore.notice.routedLauncherUnavailable": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Services en écoute" + "value": "restore: this session was started through the routed launcher '%@', which is not on PATH. Resuming the agent directly instead." } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "الخدمات المستمعة" + "value": "restore: このセッションはルーティングされたランチャー「%@」経由で開始されましたが、PATH にありません。代わりにエージェントを直接再開します。" } }, - "es": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Servicios en escucha" + "value": "restore:此会话是通过路由启动器“%@”启动的,但它不在 PATH 中。将改为直接恢复该代理。" } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "監聽中的服務" + "value": "restore: Diese Sitzung wurde über den weitergeleiteten Launcher „%@“ gestartet, der nicht im PATH liegt. Der Agent wird stattdessen direkt fortgesetzt." } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "监听中的服务" + "value": "restore : cette session a été lancée via le lanceur routé « %@ », qui est introuvable dans le PATH. Reprise directe de l'agent à la place." } }, - "ko": { + "ar": { "stringUnit": { "state": "translated", - "value": "수신 대기 서비스" + "value": "restore: بدأت هذه الجلسة عبر المشغّل الموجَّه '%@'، وهو غير موجود في PATH. سيتم استئناف الوكيل مباشرةً بدلاً من ذلك." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "待ち受け中のサービス" + "value": "restore: esta sesión se inició mediante el lanzador enrutado '%@', que no está en el PATH. Se reanudará el agente directamente." } }, - "bs": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Servisi koji slušaju" + "value": "restore:此工作階段是透過路由啟動器「%@」啟動的,但它不在 PATH 中。將改為直接繼續該代理程式。" } }, - "da": { + "ko": { "stringUnit": { "state": "translated", - "value": "Lyttende tjenester" + "value": "restore: 이 세션은 라우팅된 런처 '%@'를 통해 시작되었지만 PATH에 없습니다. 대신 에이전트를 직접 재개합니다." } - }, - "it": { + } + } + }, + "sidebar.extensions.action.noSpaceForPane": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Servizi in ascolto" + "value": "No space for new pane" } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "សេវាកម្មដែលកំពុងស្តាប់" + "value": "لا توجد مساحة للوحة جديدة" } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Lyttende tjenester" + "value": "Kein Platz für einen neuen Bereich" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Nasłuchujące usługi" + "value": "No hay espacio para un panel nuevo" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Serviços em escuta" + "value": "Pas de place pour un nouveau panneau" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Прослушивающие службы" + "value": "新しいペインを置く場所がありません" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "บริการที่กำลังรับฟัง" + "value": "새 창을 위한 공간이 없습니다" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Dinleyen hizmetler" + "value": "没有空间容纳新窗格" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Служби, що прослуховують" + "value": "沒有空間容納新窗格" } } } }, - "cloudTree.ports.binding.detail": { + "cli.focus.defaultHelp": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux’s browser route connects to 127.0.0.1 on the machine. Bind the service to 127.0.0.1 or 0.0.0.0, then refresh. Turning on Cloud VPN does not change this route." + "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": "Die Browser-Verbindung von cmux verwendet 127.0.0.1 auf der Maschine. Binden Sie den Dienst an 127.0.0.1 oder 0.0.0.0 und aktualisieren Sie. Cloud-VPN ändert diese Route nicht." + "value": "Standard: bei interaktiver Ausführung fokussieren, bei Ausführung durch einen Agenten oder ein Skript im Hintergrund bleiben." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Le navigateur cmux se connecte à 127.0.0.1 sur la machine. Liez le service à 127.0.0.1 ou 0.0.0.0, puis actualisez. Activer le VPN Cloud ne change pas cette route." + "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": "يتصل مسار متصفح cmux بالعنوان 127.0.0.1 على الجهاز. اربط الخدمة بالعنوان 127.0.0.1 أو 0.0.0.0 ثم حدّث. تشغيل Cloud VPN لا يغير هذا المسار." + "value": "الافتراضي: التركيز عند التشغيل التفاعلي، والبقاء في الخلفية عند التشغيل بواسطة وكيل أو برنامج نصي." } }, "es": { "stringUnit": { "state": "translated", - "value": "La ruta del navegador de cmux conecta a 127.0.0.1 en la máquina. Enlaza el servicio a 127.0.0.1 o 0.0.0.0 y actualiza. Activar Cloud VPN no cambia esta ruta." + "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": "cmux 的瀏覽器路徑會連線到機器上的 127.0.0.1。請將服務繫結至 127.0.0.1 或 0.0.0.0 後重新整理。開啟 Cloud VPN 不會改變此路徑。" + "value": "預設:互動執行時取得焦點;由代理程式或指令稿執行時保持在背景。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 的浏览器路由连接到机器上的 127.0.0.1。请将服务绑定到 127.0.0.1 或 0.0.0.0 后刷新。开启 Cloud VPN 不会改变此路由。" + "value": "默认:交互式运行时获得焦点;由代理或脚本运行时保持在后台。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux 브라우저 경로는 머신의 127.0.0.1에 연결합니다. 서비스를 127.0.0.1 또는 0.0.0.0에 바인딩한 후 새로 고치세요. Cloud VPN을 켜도 이 경로는 바뀌지 않습니다." + "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": "cmux のブラウザはマシン上の 127.0.0.1 に接続します。サービスを 127.0.0.1 または 0.0.0.0 にバインドして更新してください。Cloud VPN を有効にしてもこの経路は変わりません。" + "value": "フォーカス:\n --focus 新しいワークスペースに切り替えます\n --no-focus バックグラウンドで開きます" } }, - "bs": { + "de": { "stringUnit": { "state": "translated", - "value": "Ruta preglednika u cmux-u povezuje se na 127.0.0.1 na mašini. Vežite servis na 127.0.0.1 ili 0.0.0.0, zatim osvježite. Uključivanje Cloud VPN-a ne mijenja ovu rutu." + "value": "Fokus:\n --focus Zum neuen Workspace wechseln\n --no-focus Im Hintergrund öffnen" } }, - "da": { + "fr": { "stringUnit": { "state": "translated", - "value": "cmux’ browserrute forbinder til 127.0.0.1 på maskinen. Bind tjenesten til 127.0.0.1 eller 0.0.0.0, og opdater derefter. Aktivering af Cloud VPN ændrer ikke denne rute." + "value": "Focus :\n --focus Bascule vers le nouvel espace de travail\n --no-focus L’ouvre en arrière-plan" } }, - "it": { + "ar": { "stringUnit": { "state": "translated", - "value": "Il percorso del browser di cmux si connette a 127.0.0.1 sulla macchina. Associa il servizio a 127.0.0.1 o 0.0.0.0, quindi aggiorna. L’attivazione della VPN Cloud non modifica questo percorso." + "value": "التركيز:\n --focus ينتقل إلى مساحة العمل الجديدة\n --no-focus يفتحها في الخلفية" } }, - "km": { + "es": { "stringUnit": { "state": "translated", - "value": "ផ្លូវកម្មវិធីរុករករបស់ cmux ភ្ជាប់ទៅ 127.0.0.1 នៅលើម៉ាស៊ីន។ ចងសេវាកម្មទៅ 127.0.0.1 ឬ 0.0.0.0 រួចផ្ទុកឡើងវិញ។ ការបើក Cloud VPN មិនផ្លាស់ប្តូរផ្លូវនេះទេ។" + "value": "Foco:\n --focus Cambia al nuevo espacio de trabajo\n --no-focus Lo abre en segundo plano" } }, - "nb": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux sin nettleserrute kobler til 127.0.0.1 på maskinen. Bind tjenesten til 127.0.0.1 eller 0.0.0.0, og oppdater deretter. Å slå på Cloud VPN endrer ikke denne ruten." + "value": "焦點:\n --focus 切換到新的工作區\n --no-focus 在背景開啟" } }, - "pl": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Trasa przeglądarki cmux łączy się z 127.0.0.1 na maszynie. Powiąż usługę z 127.0.0.1 lub 0.0.0.0, a następnie odśwież. Włączenie Cloud VPN nie zmienia tej trasy." + "value": "焦点:\n --focus 切换到新的工作区\n --no-focus 在后台打开" } }, - "pt-BR": { + "ko": { "stringUnit": { "state": "translated", - "value": "A rota de navegador do cmux se conecta a 127.0.0.1 na máquina. Vincule o serviço a 127.0.0.1 ou 0.0.0.0 e atualize. Ativar o Cloud VPN não altera essa rota." + "value": "포커스:\n --focus 새 워크스페이스로 전환합니다\n --no-focus 백그라운드에서 엽니다" + } + } + } + }, + "settings.app.compactAgentStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Compact Agent Status" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "Браузерный маршрут cmux подключается к 127.0.0.1 на машине. Привяжите службу к 127.0.0.1 или 0.0.0.0, затем обновите. Включение Cloud VPN не меняет этот маршрут." + "value": "حالة الوكيل المختصرة" } }, - "th": { + "de": { "stringUnit": { "state": "translated", - "value": "เส้นทางเบราว์เซอร์ของ cmux เชื่อมต่อกับ 127.0.0.1 บนเครื่อง ผูกบริการกับ 127.0.0.1 หรือ 0.0.0.0 แล้วรีเฟรช การเปิด Cloud VPN ไม่ได้เปลี่ยนเส้นทางนี้" + "value": "Kompakter Agentenstatus" } }, - "tr": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux’un tarayıcı yolu makinedeki 127.0.0.1 adresine bağlanır. Hizmeti 127.0.0.1 veya 0.0.0.0 adresine bağlayın, ardından yenileyin. Cloud VPN’i açmak bu yolu değiştirmez." + "value": "Estado de agente compacto" } }, - "uk": { + "fr": { "stringUnit": { "state": "translated", - "value": "Браузерний маршрут cmux підключається до 127.0.0.1 на машині. Прив’яжіть службу до 127.0.0.1 або 0.0.0.0, потім оновіть. Увімкнення Cloud VPN не змінює цей маршрут." + "value": "Statut d’agent compact" } - } - } - }, - "cloudTree.ports.binding.title": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ja": { "stringUnit": { "state": "translated", - "value": "Services use another interface" + "value": "エージェント状態のコンパクト表示" } }, - "de": { + "ko": { "stringUnit": { "state": "translated", - "value": "Dienste verwenden eine andere Schnittstelle" + "value": "간결한 에이전트 상태" } }, - "fr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Les services utilisent une autre interface" + "value": "精简代理状态" } }, - "ar": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "تستخدم الخدمات واجهة أخرى" + "value": "精簡代理狀態" + } + } + } + }, + "settings.app.compactAgentStatus.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show a workspace's agent, unread and pull request state as one colored icon before the title instead of separate rows. Hover the icon for details." } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "Los servicios usan otra interfaz" + "value": "إظهار حالة الوكيل والعناصر غير المقروءة وطلب السحب لمساحة العمل كأيقونة ملونة واحدة قبل العنوان بدلاً من صفوف منفصلة. مرّر المؤشر فوق الأيقونة لعرض التفاصيل." } }, - "zh-Hant": { + "de": { "stringUnit": { "state": "translated", - "value": "服務使用其他介面" + "value": "Agenten-, Ungelesen- und Pull-Request-Status eines Workspace als ein farbiges Symbol vor dem Titel anzeigen statt in eigenen Zeilen. Details erscheinen, wenn du den Zeiger über das Symbol bewegst." } }, - "zh-Hans": { + "es": { "stringUnit": { "state": "translated", - "value": "服务使用其他接口" + "value": "Mostrar el estado del agente, lo no leído y la pull request de un espacio de trabajo como un único icono de color antes del título, en lugar de filas separadas. Pasa el cursor sobre el icono para ver los detalles." } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "서비스가 다른 인터페이스를 사용함" + "value": "Afficher l’état de l’agent, des éléments non lus et de la pull request d’un espace de travail sous forme d’une seule icône colorée avant le titre, au lieu de lignes distinctes. Survolez l’icône pour voir les détails." } }, "ja": { "stringUnit": { "state": "translated", - "value": "サービスが別のインターフェイスを使用しています" + "value": "ワークスペースのエージェント、未読、プルリクエストの状態を、個別の行ではなくタイトルの前の色付きアイコン1つで示します。詳細はアイコンにポインタを合わせると表示されます。" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Servisi koriste drugi interfejs" + "value": "워크스페이스의 에이전트, 읽지 않음, 풀 리퀘스트 상태를 별도의 줄이 아니라 제목 앞의 색상 아이콘 하나로 나타냅니다. 자세한 내용은 아이콘 위에 포인터를 올려 확인하세요." } }, - "da": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Tjenester bruger en anden grænseflade" + "value": "用标题前的单个彩色图标表示工作区的代理、未读和拉取请求状态,而不是各自占一行。将指针悬停在图标上可查看详细信息。" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "I servizi usano un’altra interfaccia" + "value": "以標題前的單一彩色圖示表示工作區的代理、未讀與拉取請求狀態,而非各自佔一行。將游標移到圖示上可查看詳細資訊。" + } + } + } + }, + "settings.search.alias.setting.app.compact-agent-status": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex مختصر وكيل حالة أيقونة سطر العنوان" } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "សេវាកម្មប្រើចំណុចប្រទាក់ផ្សេង" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex kompakt Agent Status Symbol Titelzeile" } }, - "nb": { + "en": { "stringUnit": { "state": "translated", - "value": "Tjenester bruker et annet grensesnitt" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Usługi używają innego interfejsu" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex compacto agente estado icono línea de título" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Os serviços usam outra interface" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex compact agent statut icône ligne de titre" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Службы используют другой интерфейс" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex コンパクト エージェント 状態 アイコン タイトル行" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "บริการใช้อินเทอร์เฟซอื่น" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex 간결 에이전트 상태 아이콘 제목 줄" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Hizmetler başka bir arabirim kullanıyor" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex 精简 代理 状态 图标 标题行" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Служби використовують інший інтерфейс" + "value": "sidebar.compactAgentStatus compact agent status running needs input glyph icon title line dense claude codex 精簡 代理 狀態 圖示 標題列" } } } }, - "cloudTree.ports.empty.noService": { + "sidebar.agentStatus.glyph.tooltip": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Start a web server on this machine, then refresh." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Starte einen Webserver auf diesem Rechner und aktualisiere dann die Liste." + "value": "%1$@: %2$@" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "Démarrez un serveur web sur cette machine, puis actualisez." + "value": "%1$@: %2$@" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "شغّل خادم ويب على هذا الجهاز، ثم حدّث القائمة." + "value": "%1$@: %2$@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Inicia un servidor web en esta máquina y luego actualiza." + "value": "%1$@: %2$@" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "在這台機器上啟動網頁伺服器,然後重新整理。" + "value": "%1$@ : %2$@" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "在此机器上启动网页服务器,然后刷新。" + "value": "%1$@:%2$@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 머신에서 웹 서버를 시작한 다음 새로 고치세요." + "value": "%1$@: %2$@" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "このマシンで Web サーバーを起動してから、更新してください。" - } - }, - "bs": { - "stringUnit": { - "state": "needs_review", - "value": "Pokrenite web server na ovoj mašini, zatim osvježite." + "value": "%1$@:%2$@" } }, - "da": { + "zh-Hant": { "stringUnit": { - "state": "needs_review", - "value": "Start en webserver på denne maskine, og opdater derefter." + "state": "translated", + "value": "%1$@:%2$@" } - }, - "it": { + } + } + }, + "sidebar.compactStatus.profile": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { - "state": "needs_review", - "value": "Avvia un server web su questa macchina, poi aggiorna." + "state": "translated", + "value": "Profile" } }, - "km": { + "ar": { "stringUnit": { - "state": "needs_review", - "value": "ចាប់ផ្តើមម៉ាស៊ីនមេបណ្តាញនៅលើម៉ាស៊ីននេះ រួចផ្ទុកឡើងវិញ។" + "state": "translated", + "value": "الملف الشخصي" } }, - "nb": { + "de": { "stringUnit": { - "state": "needs_review", - "value": "Start en webserver på denne maskinen, og oppdater deretter." + "state": "translated", + "value": "Profil" } }, - "pl": { + "es": { "stringUnit": { - "state": "needs_review", - "value": "Uruchom serwer WWW na tej maszynie, a następnie odśwież." + "state": "translated", + "value": "Perfil" } }, - "pt-BR": { + "fr": { "stringUnit": { - "state": "needs_review", - "value": "Inicie um servidor web nesta máquina e atualize." + "state": "translated", + "value": "Profil" } }, - "ru": { + "ja": { "stringUnit": { - "state": "needs_review", - "value": "Запустите веб-сервер на этой машине, затем обновите список." + "state": "translated", + "value": "プロファイル" } }, - "th": { + "ko": { "stringUnit": { - "state": "needs_review", - "value": "เริ่มเว็บเซิร์ฟเวอร์บนเครื่องนี้ แล้วรีเฟรช" + "state": "translated", + "value": "프로필" } }, - "tr": { + "zh-Hans": { "stringUnit": { - "state": "needs_review", - "value": "Bu makinede bir web sunucusu başlatın, ardından yenileyin." + "state": "translated", + "value": "配置文件" } }, - "uk": { + "zh-Hant": { "stringUnit": { - "state": "needs_review", - "value": "Запустіть вебсервер на цій машині, а потім оновіть список." + "state": "translated", + "value": "設定檔" } } } }, - "cloudTree.ports.hub.detail": { + "cli.sshPtyAttach.launchAcknowledgementTimeoutRetry": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This build cannot start cmux’s authenticated Cloud forward. Update cmux, then retry." + "value": "[cmux] cmux did not acknowledge the SSH launch; retrying %s in %ss." } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Build kann die authentifizierte Cloud-Weiterleitung von cmux nicht starten. Aktualisieren Sie cmux und versuchen Sie es erneut." + "value": "[cmux] cmux hat den SSH-Start nicht bestätigt; neuer Versuch %s in %ss." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Cette version ne peut pas démarrer le transfert Cloud authentifié de cmux. Mettez cmux à jour, puis réessayez." + "value": "[cmux] cmux n’a pas confirmé le démarrage SSH ; nouvelle tentative %s dans %ss." } }, "ar": { "stringUnit": { "state": "translated", - "value": "لا يمكن لهذا الإصدار بدء إعادة توجيه Cloud الموثّقة من cmux. حدّث cmux ثم أعد المحاولة." + "value": "[cmux] لم يؤكد cmux بدء SSH؛ ستتم إعادة المحاولة %s خلال %s ث." } }, "es": { "stringUnit": { "state": "translated", - "value": "Esta compilación no puede iniciar el reenvío autenticado de Cloud de cmux. Actualiza cmux y vuelve a intentarlo." + "value": "[cmux] cmux no confirmó el inicio de SSH; reintentando %s en %ss." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此版本無法啟動 cmux 的已驗證 Cloud 轉送。請更新 cmux 後重試。" + "value": "[cmux] cmux 未確認 SSH 啟動;將在第 %s 次於 %s 秒後重試。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此版本无法启动 cmux 的已验证 Cloud 转发。请更新 cmux 后重试。" + "value": "[cmux] cmux 未确认 SSH 启动;将在第 %s 次于 %s 秒后重试。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 빌드에서는 cmux 인증 Cloud 전달을 시작할 수 없습니다. cmux를 업데이트한 후 다시 시도하세요." + "value": "[cmux] cmux가 SSH 시작을 확인하지 못했습니다. %s번째로 %s초 후 다시 시도합니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "このビルドでは cmux の認証済み Cloud 転送を開始できません。cmux を更新して再試行してください。" + "value": "[cmux] cmux が SSH の起動を確認できないため、%s 回目を %s 秒後に再試行します。" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Ova verzija ne može pokrenuti autentificirano Cloud prosljeđivanje cmux-a. Ažurirajte cmux, zatim pokušajte ponovo." + "value": "[cmux] cmux nije potvrdio SSH pokretanje; ponovni pokušaj %s za %ss." } }, "da": { "stringUnit": { "state": "translated", - "value": "Denne build kan ikke starte cmux’ godkendte Cloud-videresendelse. Opdater cmux, og prøv igen." + "value": "[cmux] cmux bekræftede ikke SSH-starten; prøver igen %s om %ss." } }, "it": { "stringUnit": { "state": "translated", - "value": "Questa build non può avviare l’inoltro Cloud autenticato di cmux. Aggiorna cmux, quindi riprova." + "value": "[cmux] cmux non ha confermato l’avvio SSH; nuovo tentativo %s tra %ss." } }, "km": { "stringUnit": { "state": "translated", - "value": "កំណែនេះមិនអាចចាប់ផ្តើមការបញ្ជូនបន្ត Cloud ដែលបានផ្ទៀងផ្ទាត់របស់ cmux បានទេ។ ធ្វើបច្ចុប្បន្នភាព cmux រួចព្យាយាមម្តងទៀត។" + "value": "[cmux] cmux មិនបានបញ្ជាក់ការចាប់ផ្តើម SSH ទេ; កំពុងព្យាយាមម្តងទៀត %s ក្នុងរយៈពេល %ss។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Denne versjonen kan ikke starte cmux sin autentiserte Cloud-videresending. Oppdater cmux, og prøv igjen." + "value": "[cmux] cmux bekreftet ikke SSH-oppstarten; prøver på nytt %s om %ss." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Ta kompilacja nie może uruchomić uwierzytelnionego przekierowania Cloud w cmux. Zaktualizuj cmux i spróbuj ponownie." + "value": "[cmux] cmux nie potwierdził uruchomienia SSH; ponawianie próby %s za %ss." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Esta versão não consegue iniciar o encaminhamento Cloud autenticado do cmux. Atualize o cmux e tente novamente." + "value": "[cmux] o cmux não confirmou o início do SSH; tentando novamente %s em %ss." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Эта сборка не может запустить аутентифицированное перенаправление Cloud в cmux. Обновите cmux и повторите попытку." + "value": "[cmux] cmux не подтвердил запуск SSH; повторная попытка %s через %ss." } }, "th": { "stringUnit": { "state": "translated", - "value": "บิลด์นี้ไม่สามารถเริ่มการส่งต่อ Cloud ที่ยืนยันตัวตนแล้วของ cmux ได้ อัปเดต cmux แล้วลองอีกครั้ง" + "value": "[cmux] cmux ไม่ยืนยันการเริ่มต้น SSH; กำลังลองใหม่ครั้งที่ %s ในอีก %s วินาที" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu derleme cmux’un kimliği doğrulanmış Cloud yönlendirmesini başlatamıyor. cmux’u güncelleyin, ardından yeniden deneyin." + "value": "[cmux] cmux SSH başlatmasını onaylamadı; %s saniye içinde %s. deneme yapılıyor." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Ця збірка не може запустити автентифіковане перенаправлення Cloud у cmux. Оновіть cmux і повторіть спробу." + "value": "[cmux] cmux не підтвердив запуск SSH; повторна спроба %s через %s с." } } } }, - "cloudTree.ports.link.detail": { + "cloudTree.port.openInCmux": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Port discovery could not connect to this machine. Existing cmux ports may still work. Refresh to reconnect; Cloud VPN is not required." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Porterkennung konnte diese Maschine nicht erreichen. Bereits geöffnete cmux-Ports funktionieren möglicherweise weiter. Aktualisieren Sie zum erneuten Verbinden; Cloud-VPN ist nicht erforderlich." + "value": "Open in cmux" } }, - "fr": { + "ar": { "stringUnit": { "state": "translated", - "value": "La découverte des ports n’a pas pu se connecter à cette machine. Les ports cmux existants peuvent encore fonctionner. Actualisez pour reconnecter ; le VPN Cloud n’est pas nécessaire." + "value": "فتح في cmux" } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "تعذر اتصال اكتشاف المنافذ بهذا الجهاز. قد تظل منافذ cmux الحالية تعمل. حدّث لإعادة الاتصال؛ Cloud VPN غير مطلوب." + "value": "In cmux öffnen" } }, "es": { "stringUnit": { "state": "translated", - "value": "El descubrimiento de puertos no pudo conectar con esta máquina. Los puertos de cmux existentes pueden seguir funcionando. Actualiza para reconectar; Cloud VPN no es necesario." + "value": "Abrir en cmux" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "連接埠探索無法連線到此機器。現有 cmux 連接埠可能仍可使用。請重新整理以重新連線;不需要 Cloud VPN。" + "value": "Ouvrir dans cmux" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "端口发现无法连接到此机器。现有 cmux 端口可能仍可使用。请刷新以重新连接;无需 Cloud VPN。" + "value": "cmuxで開く" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포트 검색이 이 머신에 연결하지 못했습니다. 기존 cmux 포트는 계속 작동할 수 있습니다. 새로 고쳐 다시 연결하세요. Cloud VPN은 필요하지 않습니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ポート検出がこのマシンに接続できませんでした。既存の cmux ポートは引き続き動作する可能性があります。更新して再接続してください。Cloud VPN は不要です。" + "value": "cmux에서 열기" } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Otkrivanje portova nije se moglo povezati s ovom mašinom. Postojeći cmux portovi možda i dalje rade. Osvježite za ponovno povezivanje; Cloud VPN nije potreban." + "value": "在 cmux 中打开" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Portregistrering kunne ikke oprette forbindelse til denne maskine. Eksisterende cmux-porte virker muligvis stadig. Opdater for at genoprette forbindelsen; Cloud VPN er ikke påkrævet." + "value": "在 cmux 中開啟" } - }, - "it": { + } + } + }, + "cloudTree.port.openInCmux.help": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Il rilevamento delle porte non è riuscito a connettersi a questa macchina. Le porte cmux esistenti potrebbero funzionare ancora. Aggiorna per riconnetterti; la VPN Cloud non è necessaria." + "value": "Open in cmux. No VPN setup needed." } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "ការស្វែងរកច្រកមិនអាចភ្ជាប់ទៅម៉ាស៊ីននេះបានទេ។ ច្រក cmux ដែលមានស្រាប់អាចនៅតែដំណើរការ។ ផ្ទុកឡើងវិញដើម្បីភ្ជាប់ម្តងទៀត; មិនត្រូវការ Cloud VPN ទេ។" + "value": "افتح في cmux. لا يلزم إعداد VPN." } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Portoppdagelse kunne ikke koble til denne maskinen. Eksisterende cmux-porter kan fortsatt fungere. Oppdater for å koble til på nytt; Cloud VPN er ikke nødvendig." + "value": "In cmux öffnen. Keine VPN-Einrichtung nötig." } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Wykrywanie portów nie mogło połączyć się z tą maszyną. Istniejące porty cmux mogą nadal działać. Odśwież, aby połączyć ponownie; Cloud VPN nie jest wymagany." + "value": "Abrir en cmux. No hace falta configurar una VPN." } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "A descoberta de portas não conseguiu se conectar a esta máquina. As portas existentes do cmux ainda podem funcionar. Atualize para reconectar; o Cloud VPN não é necessário." + "value": "Ouvrir dans cmux. Aucune configuration VPN nécessaire." } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Обнаружению портов не удалось подключиться к этой машине. Существующие порты cmux могут по-прежнему работать. Обновите, чтобы переподключиться; Cloud VPN не требуется." + "value": "cmux で開きます。VPN の設定は不要です。" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "การค้นหาพอร์ตไม่สามารถเชื่อมต่อกับเครื่องนี้ได้ พอร์ต cmux ที่มีอยู่อาจยังใช้งานได้ รีเฟรชเพื่อเชื่อมต่อใหม่ ไม่จำเป็นต้องใช้ Cloud VPN" + "value": "cmux에서 열기. VPN 설정이 필요하지 않습니다." } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Port keşfi bu makineye bağlanamadı. Mevcut cmux portları hâlâ çalışıyor olabilir. Yeniden bağlanmak için yenileyin; Cloud VPN gerekmez." + "value": "在 cmux 中打开。无需设置 VPN。" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Виявленню портів не вдалося підключитися до цієї машини. Наявні порти cmux можуть і далі працювати. Оновіть, щоб підключитися знову; Cloud VPN не потрібен." + "value": "在 cmux 中開啟。無需設定 VPN。" } } } }, - "cloudTree.ports.link.title": { + "cloudTree.port.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Cloud link unavailable" + "value": "Port %@" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Cloud-Verbindung nicht verfügbar" + "value": "المنفذ %@" } }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Connexion Cloud indisponible" - } - }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "اتصال Cloud غير متاح" + "value": "Port-Nr. %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Conexión de Cloud no disponible" + "value": "Puerto %@" } }, - "zh-Hant": { + "fr": { "stringUnit": { "state": "translated", - "value": "Cloud 連線無法使用" + "value": "Port n° %@" } }, - "zh-Hans": { + "ja": { "stringUnit": { "state": "translated", - "value": "Cloud 连接不可用" + "value": "ポート %@" } }, "ko": { "stringUnit": { "state": "translated", - "value": "Cloud 연결을 사용할 수 없음" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud 接続を利用できません" + "value": "포트 %@" } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud veza nije dostupna" + "value": "端口 %@" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Cloud-forbindelse utilgængelig" + "value": "連接埠 %@" } - }, - "it": { + } + } + }, + "cloudTree.placeholder.deviceOffline": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Collegamento Cloud non disponibile" + "value": "Offline — its workspaces return when it does" } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "តំណ Cloud មិនអាចប្រើបាន" + "value": "غير متصل — ستعود مساحات عمله عند اتصاله مجددًا" } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Cloud-tilkobling utilgjengelig" + "value": "Offline — die Arbeitsbereiche erscheinen wieder, sobald das Gerät online ist" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Połączenie Cloud niedostępne" + "value": "Sin conexión — sus espacios de trabajo volverán cuando se conecte" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Conexão Cloud indisponível" + "value": "Hors ligne — ses espaces de travail réapparaîtront à sa reconnexion" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Связь с Cloud недоступна" + "value": "オフライン — 再接続するとワークスペースが戻ります" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "ลิงก์ Cloud ไม่พร้อมใช้งาน" + "value": "오프라인 — 다시 연결되면 작업 공간이 돌아옵니다" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Cloud bağlantısı kullanılamıyor" + "value": "离线 — 重新连接后会恢复工作区" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Зв’язок із Cloud недоступний" + "value": "離線 — 重新連線後會恢復工作區" } } } }, - "cloudTree.ports.loading.detail": { + "socket.mainActorHop.timeout.mayHaveRun": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Checking services through cmux’s authenticated Cloud link. No system VPN is needed." + "value": "cmux did not respond within 10 seconds after the command started, so its result is unknown. Check the effect before retrying." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コマンドの開始後 10 秒以内に cmux が応答しなかったため、結果は不明です。再試行する前に効果を確認してください。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "命令开始后 10 秒内 cmux 没有响应,因此结果未知。请先确认效果再重试。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dienste werden über die authentifizierte Cloud-Verbindung von cmux geprüft. Ein System-VPN ist nicht erforderlich." + "value": "cmux hat innerhalb von 10 Sekunden nach dem Start des Befehls nicht geantwortet, das Ergebnis ist daher unbekannt. Prüfe die Auswirkung, bevor du es erneut versuchst." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Vérification des services via la connexion Cloud authentifiée de cmux. Aucun VPN système n’est nécessaire." + "value": "cmux n’a pas répondu dans les 10 secondes suivant le démarrage de la commande, son résultat est donc inconnu. Vérifiez l’effet avant de réessayer." } }, "ar": { "stringUnit": { "state": "translated", - "value": "جارٍ فحص الخدمات عبر اتصال Cloud الموثّق من cmux. لا حاجة إلى VPN النظام." + "value": "لم يستجب cmux خلال 10 ثوانٍ بعد بدء الأمر، لذا فإن نتيجته غير معروفة. تحقق من الأثر قبل إعادة المحاولة." } }, "es": { "stringUnit": { "state": "translated", - "value": "Comprobando servicios mediante la conexión autenticada de Cloud de cmux. No se necesita una VPN del sistema." + "value": "cmux no respondió en los 10 segundos posteriores al inicio del comando, por lo que su resultado es desconocido. Comprueba el efecto antes de reintentar." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "正在透過 cmux 的已驗證 Cloud 連線檢查服務。不需要系統 VPN。" + "value": "命令開始後 10 秒內 cmux 沒有回應,因此結果未知。請先確認效果再重試。" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "正在通过 cmux 的已验证 Cloud 连接检查服务。无需系统 VPN。" + "value": "명령이 시작된 후 10초 안에 cmux가 응답하지 않아 결과를 알 수 없습니다. 다시 시도하기 전에 효과를 확인하세요." } - }, - "ko": { + } + } + }, + "socket.mainActorHop.timeout.notRun": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "cmux의 인증된 Cloud 연결로 서비스를 확인하고 있습니다. 시스템 VPN은 필요하지 않습니다." + "value": "cmux did not respond within 10 seconds, so the command was not run. Retry in a moment." } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux の認証済み Cloud 接続でサービスを確認しています。システム VPN は不要です。" + "value": "cmux が 10 秒以内に応答しなかったため、コマンドは実行されませんでした。しばらくしてから再試行してください。" } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Provjera servisa putem autentificirane Cloud veze cmux-a. Sistemski VPN nije potreban." + "value": "cmux 在 10 秒内没有响应,因此未运行该命令。请稍后重试。" } }, - "da": { + "de": { "stringUnit": { "state": "translated", - "value": "Kontrollerer tjenester via cmux’ godkendte Cloud-forbindelse. Der kræves ingen system-VPN." + "value": "cmux hat innerhalb von 10 Sekunden nicht geantwortet, der Befehl wurde daher nicht ausgeführt. Versuche es gleich noch einmal." } }, - "it": { + "fr": { "stringUnit": { "state": "translated", - "value": "Controllo dei servizi tramite il collegamento Cloud autenticato di cmux. Non serve una VPN di sistema." + "value": "cmux n’a pas répondu dans les 10 secondes, la commande n’a donc pas été exécutée. Réessayez dans un instant." } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "កំពុងពិនិត្យសេវាកម្មតាមរយៈតំណ Cloud ដែលបានផ្ទៀងផ្ទាត់របស់ cmux។ មិនត្រូវការ VPN របស់ប្រព័ន្ធទេ។" + "value": "لم يستجب cmux خلال 10 ثوانٍ، لذا لم يتم تنفيذ الأمر. أعد المحاولة بعد لحظات." } }, - "nb": { + "es": { "stringUnit": { "state": "translated", - "value": "Sjekker tjenester via cmux sin autentiserte Cloud-tilkobling. Ingen system-VPN er nødvendig." + "value": "cmux no respondió en 10 segundos, por lo que el comando no se ejecutó. Reintenta en un momento." } }, - "pl": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Sprawdzanie usług przez uwierzytelnione połączenie Cloud w cmux. Systemowa sieć VPN nie jest potrzebna." + "value": "cmux 在 10 秒內沒有回應,因此未執行該命令。請稍後重試。" } }, - "pt-BR": { + "ko": { "stringUnit": { "state": "translated", - "value": "Verificando serviços pela conexão Cloud autenticada do cmux. Nenhuma VPN do sistema é necessária." + "value": "cmux가 10초 안에 응답하지 않아 명령이 실행되지 않았습니다. 잠시 후 다시 시도하세요." } - }, - "ru": { + } + } + }, + "socket.overloaded.message": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Проверка служб через аутентифицированную связь cmux с Cloud. Системный VPN не нужен." + "value": "cmux is handling too many control-socket requests right now, so this one was not run. Retry in a moment." } }, - "th": { + "ja": { "stringUnit": { "state": "translated", - "value": "กำลังตรวจสอบบริการผ่านลิงก์ Cloud ที่ยืนยันตัวตนแล้วของ cmux ไม่ต้องใช้ VPN ของระบบ" + "value": "cmux は現在、制御ソケットの要求を処理しきれないため、この要求は実行されませんでした。しばらくしてから再試行してください。" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Hizmetler cmux’un kimliği doğrulanmış Cloud bağlantısı üzerinden denetleniyor. Sistem VPN’i gerekmez." + "value": "cmux 当前正在处理过多的控制套接字请求,因此未运行此请求。请稍后重试。" } }, - "uk": { + "de": { "stringUnit": { "state": "translated", - "value": "Перевірка служб через автентифікований зв’язок cmux із Cloud. Системний VPN не потрібен." + "value": "cmux verarbeitet gerade zu viele Control-Socket-Anfragen, diese wurde daher nicht ausgeführt. Versuche es gleich noch einmal." } - } - } - }, - "cloudTree.ports.loopback.detail": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "fr": { "stringUnit": { "state": "translated", - "value": "These services open in cmux without Cloud VPN. They listen on the machine’s loopback address, so other apps cannot reach them through its private IP, even with VPN connected." + "value": "cmux traite actuellement trop de requêtes du socket de contrôle, celle-ci n’a donc pas été exécutée. Réessayez dans un instant." } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Diese Dienste lassen sich in cmux ohne Cloud-VPN öffnen. Sie lauschen auf der Loopback-Adresse der Maschine. Andere Apps erreichen sie daher auch mit VPN nicht über deren private IP." + "value": "يعالج cmux حاليًا عددًا كبيرًا جدًا من طلبات مقبس التحكم، لذا لم يتم تنفيذ هذا الطلب. أعد المحاولة بعد لحظات." } }, - "fr": { + "es": { "stringUnit": { "state": "translated", - "value": "Ces services s’ouvrent dans cmux sans VPN Cloud. Ils écoutent sur l’adresse locale de la machine : les autres apps ne peuvent pas y accéder via son IP privée, même avec le VPN." + "value": "cmux está gestionando demasiadas solicitudes del socket de control ahora mismo, por lo que esta no se ejecutó. Reintenta en un momento." } }, - "ar": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "تُفتح هذه الخدمات داخل cmux دون Cloud VPN. وهي تستمع على عنوان الواجهة المحلية للجهاز، لذا لا تستطيع التطبيقات الأخرى الوصول إليها عبر عنوان IP الخاص به حتى مع اتصال VPN." + "value": "cmux 目前正在處理過多的控制 socket 請求,因此未執行此請求。請稍後重試。" } }, - "es": { + "ko": { "stringUnit": { "state": "translated", - "value": "Estos servicios se abren en cmux sin Cloud VPN. Escuchan en la dirección de bucle local de la máquina, por lo que otras apps no pueden acceder por su IP privada, incluso con la VPN conectada." + "value": "cmux가 지금 너무 많은 제어 소켓 요청을 처리하고 있어 이 요청은 실행되지 않았습니다. 잠시 후 다시 시도하세요." + } + } + } + }, + "socket.request.cancelled": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Request was cancelled" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "這些服務可在 cmux 中開啟,不需要 Cloud VPN。它們在機器的迴路位址上監聽,因此即使已連線 VPN,其他 App 也無法透過其私人 IP 存取。" + "value": "リクエストはキャンセルされました" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "这些服务可在 cmux 中打开,无需 Cloud VPN。它们监听机器的回环地址,因此即使已连接 VPN,其他应用也无法通过其专用 IP 访问。" + "value": "请求已取消" } }, - "ko": { + "de": { "stringUnit": { "state": "translated", - "value": "이 서비스는 Cloud VPN 없이 cmux에서 열립니다. 머신의 루프백 주소에서 수신 대기하므로 VPN에 연결해도 다른 앱은 비공개 IP로 접근할 수 없습니다." + "value": "Die Anfrage wurde abgebrochen" } }, - "ja": { + "fr": { "stringUnit": { "state": "translated", - "value": "これらのサービスは Cloud VPN なしで cmux から開けます。マシンのループバックアドレスで待ち受けるため、VPN 接続中でも他のアプリからプライベート IP 経由ではアクセスできません。" + "value": "La requête a été annulée" } }, - "bs": { + "ar": { "stringUnit": { "state": "translated", - "value": "Ovi servisi se otvaraju u cmux-u bez Cloud VPN-a. Slušaju na loopback adresi mašine, pa ih druge aplikacije ne mogu dosegnuti preko njene privatne IP adrese, čak ni uz povezan VPN." + "value": "تم إلغاء الطلب" } }, - "da": { + "es": { "stringUnit": { "state": "translated", - "value": "Disse tjenester åbnes i cmux uden Cloud VPN. De lytter på maskinens loopback-adresse, så andre apps kan ikke nå dem via dens private IP, heller ikke med VPN forbundet." + "value": "Se canceló la solicitud" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Questi servizi si aprono in cmux senza VPN Cloud. Sono in ascolto sull’indirizzo di loopback della macchina, quindi le altre app non possono raggiungerli tramite il suo IP privato, nemmeno con la VPN connessa." + "value": "請求已取消" } }, - "km": { + "ko": { "stringUnit": { "state": "translated", - "value": "សេវាកម្មទាំងនេះបើកនៅក្នុង cmux ដោយមិនត្រូវការ Cloud VPN។ ពួកវាស្តាប់នៅលើអាសយដ្ឋាន loopback របស់ម៉ាស៊ីន ដូច្នេះកម្មវិធីផ្សេងទៀតមិនអាចចូលដល់ពួកវាតាមរយៈ IP ឯកជនរបស់វាបានទេ ទោះបីភ្ជាប់ VPN ក៏ដោយ។" + "value": "요청이 취소되었습니다" + } + } + } + }, + "socket.request.failedBeforeResult": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Request failed before returning a result" } }, - "nb": { + "ja": { "stringUnit": { "state": "translated", - "value": "Disse tjenestene åpnes i cmux uten Cloud VPN. De lytter på maskinens loopback-adresse, så andre apper kan ikke nå dem via den private IP-adressen, selv med VPN tilkoblet." + "value": "結果を返す前にリクエストが失敗しました" } }, - "pl": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Te usługi otwierają się w cmux bez Cloud VPN. Nasłuchują na adresie pętli zwrotnej maszyny, więc inne aplikacje nie mogą do nich dotrzeć przez jej prywatny adres IP, nawet przy połączonym VPN." + "value": "请求在返回结果之前失败" } }, - "pt-BR": { + "de": { "stringUnit": { "state": "translated", - "value": "Esses serviços abrem no cmux sem o Cloud VPN. Eles escutam no endereço de loopback da máquina, então outros apps não conseguem alcançá-los pelo IP privado dela, mesmo com a VPN conectada." + "value": "Die Anfrage ist fehlgeschlagen, bevor ein Ergebnis zurückgegeben wurde" } }, - "ru": { + "fr": { "stringUnit": { "state": "translated", - "value": "Эти службы открываются в cmux без Cloud VPN. Они прослушивают loopback-адрес машины, поэтому другие приложения не могут обратиться к ним через её частный IP даже при подключённом VPN." + "value": "La requête a échoué avant de renvoyer un résultat" } }, - "th": { + "ar": { "stringUnit": { "state": "translated", - "value": "บริการเหล่านี้เปิดใน cmux ได้โดยไม่ต้องใช้ Cloud VPN บริการรับฟังบนที่อยู่ loopback ของเครื่อง แอปอื่นจึงเข้าถึงผ่าน IP ส่วนตัวของเครื่องไม่ได้ แม้จะเชื่อมต่อ VPN แล้วก็ตาม" + "value": "فشل الطلب قبل إرجاع نتيجة" } }, - "tr": { + "es": { "stringUnit": { "state": "translated", - "value": "Bu hizmetler cmux’ta Cloud VPN olmadan açılır. Makinenin geri döngü adresinde dinledikleri için diğer uygulamalar, VPN bağlıyken bile makinenin özel IP’si üzerinden onlara ulaşamaz." + "value": "La solicitud falló antes de devolver un resultado" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Ці служби відкриваються в cmux без Cloud VPN. Вони прослуховують loopback-адресу машини, тому інші застосунки не можуть дістатися до них через її приватну IP-адресу, навіть із підключеним VPN." + "value": "請求在傳回結果之前失敗" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "결과를 반환하기 전에 요청이 실패했습니다" } } } }, - "cloudTree.ports.loopback.title": { + "agent.codex.fork.notice.body": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Loopback services available" + "value": "cmux could not identify the new fork session. Start the fork again from the parent pane." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux は新しいフォーク セッションを特定できませんでした。親ペインからもう一度フォークしてください。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Loopback-Dienste verfügbar" + "value": "cmux konnte die neue Fork-Sitzung nicht erkennen. Starten Sie den Fork erneut im übergeordneten Pane." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Services locaux disponibles" + "value": "cmux n’a pas pu identifier la nouvelle session forkée. Relancez le fork depuis le volet parent." } }, "ar": { "stringUnit": { "state": "translated", - "value": "خدمات الواجهة المحلية متاحة" + "value": "تعذر على cmux تحديد جلسة التفريع الجديدة. ابدأ التفريع مرة أخرى من الجزء الأصلي." } }, "es": { "stringUnit": { "state": "translated", - "value": "Servicios de bucle local disponibles" + "value": "cmux no pudo identificar la nueva sesión bifurcada. Inicia el fork de nuevo desde el panel principal." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "可使用迴路服務" + "value": "cmux 無法識別新的分支工作階段。請從父窗格重新開始分支。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "回环服务可用" + "value": "cmux 无法识别新的分支会话。请从父窗格重新开始分支。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "루프백 서비스 사용 가능" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ループバックのサービスを利用できます" + "value": "cmux가 새 포크 세션을 식별하지 못했습니다. 부모 패널에서 포크를 다시 시작하세요." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Dostupni loopback servisi" + "value": "cmux nije mogao prepoznati novu sesiju račvanja. Ponovo pokrenite račvanje iz nadređenog okna." } }, "da": { "stringUnit": { "state": "translated", - "value": "Loopback-tjenester tilgængelige" + "value": "cmux kunne ikke identificere den nye forgreningssession. Start forgreningen igen fra forældreruden." } }, "it": { "stringUnit": { "state": "translated", - "value": "Servizi di loopback disponibili" + "value": "cmux non ha potuto identificare la nuova sessione fork. Avvia di nuovo il fork dal riquadro principale." } }, "km": { "stringUnit": { "state": "translated", - "value": "មានសេវាកម្ម loopback" + "value": "cmux មិនអាចកំណត់អត្តសញ្ញាណសម័យ fork ថ្មីបានទេ។ ចាប់ផ្ដើម fork ម្ដងទៀតពីផ្ទាំងមេ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Loopback-tjenester tilgjengelige" + "value": "cmux kunne ikke identifisere den nye forgreningsøkten. Start forgreningen på nytt fra den overordnede ruten." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Dostępne usługi pętli zwrotnej" + "value": "cmux nie mógł zidentyfikować nowej sesji forka. Uruchom fork ponownie z panelu nadrzędnego." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Serviços de loopback disponíveis" + "value": "O cmux não conseguiu identificar a nova sessão bifurcada. Inicie o fork novamente no painel principal." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Доступны loopback-службы" + "value": "cmux не удалось определить новый сеанс форка. Запустите форк снова из родительской панели." } }, "th": { "stringUnit": { "state": "translated", - "value": "มีบริการ loopback พร้อมใช้งาน" + "value": "cmux ไม่สามารถระบุเซสชันที่แยกใหม่ได้ เริ่มการแยกอีกครั้งจากบานหน้าต่างหลัก" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Geri döngü hizmetleri kullanılabilir" + "value": "cmux yeni fork oturumunu belirleyemedi. Fork'u üst panelden yeniden başlatın." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Доступні loopback-служби" + "value": "cmux не вдалося визначити новий сеанс форка. Запустіть форк ще раз із батьківської панелі." } } } }, - "cloudTree.ports.notRequested.title": { + "agent.codex.fork.notice.parentClearFailed.body": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Ports not checked yet" + "value": "cmux could not safely bind the new fork session. Retry the fork from the parent pane." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux は新しいフォーク セッションを安全にバインドできませんでした。親ペインからもう一度フォークしてください。" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ports noch nicht geprüft" + "value": "cmux konnte die neue Fork-Sitzung nicht sicher binden. Starten Sie den Fork erneut im übergeordneten Pane." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ports pas encore vérifiés" + "value": "cmux n’a pas pu lier la nouvelle session forkée en toute sécurité. Relancez le fork depuis le volet parent." } }, "ar": { "stringUnit": { "state": "translated", - "value": "لم يتم فحص المنافذ بعد" + "value": "تعذر على cmux ربط جلسة التفريع الجديدة بأمان. أعد بدء التفريع من الجزء الأصلي." } }, "es": { "stringUnit": { "state": "translated", - "value": "Puertos aún sin comprobar" + "value": "cmux no pudo vincular de forma segura la nueva sesión bifurcada. Inicia el fork de nuevo desde el panel principal." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "尚未檢查連接埠" + "value": "cmux 無法安全繫結新的分支工作階段。請從父窗格重新開始分支。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "尚未检查端口" + "value": "cmux 无法安全绑定新的分支会话。请从父窗格重新开始分支。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "아직 포트를 확인하지 않음" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ポートは未確認です" + "value": "cmux가 새 포크 세션을 안전하게 연결하지 못했습니다. 부모 패널에서 포크를 다시 시작하세요." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Portovi još nisu provjereni" + "value": "cmux nije mogao sigurno povezati novu sesiju račvanja. Ponovo pokrenite račvanje iz nadređenog okna." } }, "da": { "stringUnit": { "state": "translated", - "value": "Porte er ikke kontrolleret endnu" + "value": "cmux kunne ikke binde den nye forgreningssession sikkert. Start forgreningen igen fra forældreruden." } }, "it": { "stringUnit": { "state": "translated", - "value": "Porte non ancora controllate" + "value": "cmux non ha potuto associare in sicurezza la nuova sessione fork. Avvia di nuovo il fork dal riquadro principale." } }, "km": { "stringUnit": { "state": "translated", - "value": "មិនទាន់បានពិនិត្យច្រកនៅឡើយ" + "value": "cmux មិនអាចភ្ជាប់សម័យ fork ថ្មីដោយសុវត្ថិភាពបានទេ។ ចាប់ផ្ដើម fork ម្ដងទៀតពីផ្ទាំងមេ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Porter er ikke sjekket ennå" + "value": "cmux kunne ikke binde den nye forgreningsøkten på en trygg måte. Start forgreningen på nytt fra den overordnede ruten." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Porty nie zostały jeszcze sprawdzone" + "value": "cmux nie mógł bezpiecznie powiązać nowej sesji forka. Uruchom fork ponownie z panelu nadrzędnego." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Portas ainda não verificadas" + "value": "O cmux não conseguiu vincular com segurança a nova sessão bifurcada. Inicie o fork novamente no painel principal." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Порты ещё не проверены" + "value": "cmux не удалось безопасно привязать новый сеанс форка. Запустите форк снова из родительской панели." } }, "th": { "stringUnit": { "state": "translated", - "value": "ยังไม่ได้ตรวจสอบพอร์ต" + "value": "cmux ไม่สามารถผูกเซสชันที่แยกใหม่ได้อย่างปลอดภัย เริ่มการแยกอีกครั้งจากบานหน้าต่างหลัก" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Portlar henüz denetlenmedi" + "value": "cmux yeni fork oturumunu güvenli bir şekilde bağlayamadı. Fork'u üst panelden yeniden başlatın." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Порти ще не перевірено" + "value": "cmux не вдалося безпечно прив’язати новий сеанс форка. Запустіть форк ще раз із батьківської панелі." } } } }, - "cloudTree.ports.privateAddress": { + "agent.codex.fork.notice.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Private address unavailable" + "value": "Fork unavailable" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "フォークを利用できません" } }, "de": { "stringUnit": { "state": "translated", - "value": "Private Adresse nicht verfügbar" + "value": "Fork nicht verfügbar" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Adresse privée indisponible" + "value": "Fork indisponible" } }, "ar": { "stringUnit": { "state": "translated", - "value": "العنوان الخاص غير متاح" + "value": "التفريع غير متاح" } }, "es": { "stringUnit": { "state": "translated", - "value": "Dirección privada no disponible" + "value": "Fork no disponible" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "私人位址無法使用" + "value": "無法使用分支" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "专用地址不可用" + "value": "分支不可用" } }, "ko": { "stringUnit": { "state": "translated", - "value": "비공개 주소를 사용할 수 없음" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "プライベートアドレスを利用できません" + "value": "포크를 사용할 수 없음" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Privatna adresa nije dostupna" + "value": "Račvanje nije dostupno" } }, "da": { "stringUnit": { "state": "translated", - "value": "Privat adresse utilgængelig" + "value": "Fork ikke tilgængelig" } }, "it": { "stringUnit": { "state": "translated", - "value": "Indirizzo privato non disponibile" + "value": "Fork non disponibile" } }, "km": { "stringUnit": { "state": "translated", - "value": "អាសយដ្ឋានឯកជនមិនអាចប្រើបាន" + "value": "មិនអាចប្រើការបំបែកបាន" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Privat adresse utilgjengelig" + "value": "Fork utilgjengelig" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Adres prywatny niedostępny" + "value": "Fork niedostępny" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Endereço privado indisponível" + "value": "Fork indisponível" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Частный адрес недоступен" + "value": "Форк недоступен" } }, "th": { "stringUnit": { "state": "translated", - "value": "ไม่มีที่อยู่ส่วนตัว" + "value": "ไม่สามารถใช้การแยกสาขาได้" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Özel adres kullanılamıyor" + "value": "Fork kullanılamıyor" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Приватна адреса недоступна" + "value": "Форк недоступний" } } } }, - "cloudTree.ports.privateAddress.detail": { + "agent.codex.fork.notice.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux’s authenticated route needs this machine’s private address. Refresh to retrieve it. Turning on Cloud VPN does not assign a missing address." + "value": "Agent fork" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントフォーク" } }, "de": { "stringUnit": { "state": "translated", - "value": "Die authentifizierte Route von cmux benötigt die private Adresse dieser Maschine. Aktualisieren Sie, um sie abzurufen. Cloud-VPN weist keine fehlende Adresse zu." + "value": "Agent-Fork" } }, "fr": { "stringUnit": { "state": "translated", - "value": "La route authentifiée de cmux nécessite l’adresse privée de cette machine. Actualisez pour la récupérer. Activer le VPN Cloud n’attribue pas d’adresse manquante." + "value": "Fork d’agent" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يحتاج مسار cmux الموثّق إلى العنوان الخاص لهذا الجهاز. حدّث لاسترجاعه. تشغيل Cloud VPN لا يعيّن عنوانًا مفقودًا." + "value": "تفريع الوكيل" } }, "es": { "stringUnit": { "state": "translated", - "value": "La ruta autenticada de cmux necesita la dirección privada de esta máquina. Actualiza para obtenerla. Activar Cloud VPN no asigna una dirección que falta." + "value": "Fork del agente" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux 的已驗證路徑需要此機器的私人位址。請重新整理以取得位址。開啟 Cloud VPN 不會指派缺少的位址。" + "value": "代理程式分支" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux 的已验证路由需要此机器的专用地址。请刷新以获取地址。开启 Cloud VPN 不会分配缺失的地址。" + "value": "代理分支" } }, "ko": { "stringUnit": { "state": "translated", - "value": "cmux의 인증된 경로에는 이 머신의 비공개 주소가 필요합니다. 새로 고쳐 주소를 가져오세요. Cloud VPN을 켜도 누락된 주소가 할당되지는 않습니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux の認証済み経路にはこのマシンのプライベートアドレスが必要です。更新して取得してください。Cloud VPN を有効にしても不足しているアドレスは割り当てられません。" + "value": "에이전트 포크" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Autentificirana ruta cmux-a treba privatnu adresu ove mašine. Osvježite da je preuzmete. Uključivanje Cloud VPN-a ne dodjeljuje adresu koja nedostaje." + "value": "Račvanje agenta" } }, "da": { "stringUnit": { "state": "translated", - "value": "cmux’ godkendte rute kræver denne maskines private adresse. Opdater for at hente den. Aktivering af Cloud VPN tildeler ikke en manglende adresse." + "value": "Agentfork" } }, "it": { "stringUnit": { "state": "translated", - "value": "Il percorso autenticato di cmux richiede l’indirizzo privato di questa macchina. Aggiorna per recuperarlo. L’attivazione della VPN Cloud non assegna un indirizzo mancante." + "value": "Fork dell'agente" } }, "km": { "stringUnit": { "state": "translated", - "value": "ផ្លូវដែលបានផ្ទៀងផ្ទាត់របស់ cmux ត្រូវការអាសយដ្ឋានឯកជនរបស់ម៉ាស៊ីននេះ។ ផ្ទុកឡើងវិញដើម្បីទាញយកវា។ ការបើក Cloud VPN មិនផ្តល់អាសយដ្ឋានដែលបាត់ទេ។" + "value": "ការបំបែកភ្នាក់ងារ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "cmux sin autentiserte rute trenger denne maskinens private adresse. Oppdater for å hente den. Å slå på Cloud VPN tildeler ikke en manglende adresse." + "value": "Agentforgrening" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Uwierzytelniona trasa cmux wymaga prywatnego adresu tej maszyny. Odśwież, aby go pobrać. Włączenie Cloud VPN nie przypisuje brakującego adresu." + "value": "Fork agenta" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A rota autenticada do cmux precisa do endereço privado desta máquina. Atualize para obtê-lo. Ativar o Cloud VPN não atribui um endereço ausente." + "value": "Fork do agente" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Аутентифицированному маршруту cmux нужен частный адрес этой машины. Обновите, чтобы получить его. Включение Cloud VPN не назначает отсутствующий адрес." + "value": "Форк агента" } }, "th": { "stringUnit": { "state": "translated", - "value": "เส้นทางที่ยืนยันตัวตนแล้วของ cmux ต้องใช้ที่อยู่ส่วนตัวของเครื่องนี้ รีเฟรชเพื่อดึงข้อมูล การเปิด Cloud VPN ไม่ได้กำหนดที่อยู่ที่ขาดหายไป" + "value": "การแยกสาขาของเอเจนต์" } }, "tr": { "stringUnit": { "state": "translated", - "value": "cmux’un kimliği doğrulanmış yolu bu makinenin özel adresine ihtiyaç duyar. Adresi almak için yenileyin. Cloud VPN’i açmak eksik bir adres atamaz." + "value": "Ajan çatalı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Автентифікованому маршруту cmux потрібна приватна адреса цієї машини. Оновіть, щоб отримати її. Увімкнення Cloud VPN не призначає відсутню адресу." + "value": "Форк агента" } } } }, - "cloudTree.ports.routeNote": { + "socket.authTeam.committedStatusTimedOut": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "cmux forwards ports without Cloud VPN. Web browsers and other Mac apps need Cloud VPN to reach services bound to the machine’s private address." + "value": "The team change was applied, but cmux did not report the updated status within 10 seconds. Run `cmux auth status` to confirm." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "チームの変更は適用されましたが、cmux は 10 秒以内に更新後のステータスを報告しませんでした。`cmux auth status` を実行して確認してください。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "团队更改已应用,但 cmux 未能在 10 秒内报告更新后的状态。请运行 `cmux auth status` 确认。" } }, "de": { "stringUnit": { "state": "translated", - "value": "cmux leitet Ports ohne Cloud-VPN weiter. Webbrowser und andere Mac-Apps benötigen Cloud-VPN, um Dienste an der privaten Adresse der Maschine zu erreichen." + "value": "Die Teamänderung wurde übernommen, aber cmux hat den aktualisierten Status nicht innerhalb von 10 Sekunden gemeldet. Führe `cmux auth status` aus, um es zu prüfen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "cmux transfère les ports sans VPN Cloud. Les navigateurs web et les autres apps Mac ont besoin du VPN Cloud pour atteindre les services liés à l’adresse privée de la machine." + "value": "Le changement d’équipe a été appliqué, mais cmux n’a pas signalé le statut mis à jour dans les 10 secondes. Exécutez `cmux auth status` pour confirmer." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يعيد cmux توجيه المنافذ دون Cloud VPN. تحتاج متصفحات الويب وتطبيقات Mac الأخرى إلى Cloud VPN للوصول إلى الخدمات المرتبطة بالعنوان الخاص للجهاز." + "value": "تم تطبيق تغيير الفريق، لكن cmux لم يُبلّغ عن الحالة المحدّثة خلال 10 ثوانٍ. شغّل `cmux auth status` للتأكيد." } }, "es": { "stringUnit": { "state": "translated", - "value": "cmux reenvía puertos sin Cloud VPN. Los navegadores web y otras apps de Mac necesitan Cloud VPN para acceder a servicios enlazados a la dirección privada de la máquina." + "value": "El cambio de equipo se aplicó, pero cmux no informó el estado actualizado en 10 segundos. Ejecuta `cmux auth status` para confirmarlo." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux 可轉送連接埠,不需要 Cloud VPN。網頁瀏覽器和其他 Mac App 需要 Cloud VPN,才能存取繫結至機器私人位址的服務。" + "value": "團隊變更已套用,但 cmux 未能在 10 秒內回報更新後的狀態。請執行 `cmux auth status` 確認。" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux 可转发端口,无需 Cloud VPN。网页浏览器和其他 Mac 应用需要 Cloud VPN,才能访问绑定到机器专用地址的服务。" + "value": "팀 변경은 적용되었지만 cmux가 10초 안에 업데이트된 상태를 보고하지 않았습니다. `cmux auth status`를 실행해 확인하세요." + } + } + } + }, + "settings.mobile.browserTunnel.allowOtherHosts": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "iOS Browser Reaches Other Hosts" } }, - "ko": { + "de": { "stringUnit": { "state": "translated", - "value": "cmux는 Cloud VPN 없이 포트를 전달합니다. 웹 브라우저 및 다른 Mac 앱이 머신의 비공개 주소에 바인딩된 서비스에 접근하려면 Cloud VPN이 필요합니다." + "value": "iOS-Browser erreicht andere Hosts" } }, - "ja": { + "fr": { "stringUnit": { "state": "translated", - "value": "cmux は Cloud VPN なしでポートを転送します。Web ブラウザやその他の Mac アプリからマシンのプライベートアドレスにバインドされたサービスにアクセスするには Cloud VPN が必要です。" + "value": "Le navigateur iOS accède aux autres hôtes" } }, - "bs": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux prosljeđuje portove bez Cloud VPN-a. Web preglednicima i drugim Mac aplikacijama potreban je Cloud VPN za pristup servisima vezanim za privatnu adresu mašine." + "value": "متصفح iOS يصل إلى مضيفين آخرين" } }, - "da": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux videresender porte uden Cloud VPN. Webbrowsere og andre Mac-apps skal bruge Cloud VPN for at nå tjenester, der er bundet til maskinens private adresse." + "value": "El navegador de iOS accede a otros hosts" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux inoltra le porte senza VPN Cloud. I browser web e le altre app per Mac hanno bisogno della VPN Cloud per raggiungere i servizi associati all’indirizzo privato della macchina." + "value": "iOS 瀏覽器可連線至其他主機" } }, - "km": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux បញ្ជូនបន្តច្រកដោយមិនត្រូវការ Cloud VPN។ កម្មវិធីរុករកបណ្តាញ និងកម្មវិធី Mac ផ្សេងទៀតត្រូវការ Cloud VPN ដើម្បីចូលដល់សេវាកម្មដែលចងទៅអាសយដ្ឋានឯកជនរបស់ម៉ាស៊ីន។" + "value": "iOS 浏览器可访问其他主机" } }, - "nb": { + "ko": { "stringUnit": { "state": "translated", - "value": "cmux videresender porter uten Cloud VPN. Nettlesere og andre Mac-apper trenger Cloud VPN for å nå tjenester som er bundet til maskinens private adresse." + "value": "iOS 브라우저의 다른 호스트 접근" } }, - "pl": { + "ja": { "stringUnit": { "state": "translated", - "value": "cmux przekierowuje porty bez Cloud VPN. Przeglądarki internetowe i inne aplikacje na Maca potrzebują Cloud VPN, aby dotrzeć do usług powiązanych z prywatnym adresem maszyny." + "value": "iOSブラウザから他のホストに接続" + } + } + } + }, + "settings.mobile.browserTunnel.allowOtherHosts.subtitleOn": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The iOS browser can load LAN, VPN, and internet hosts through this Mac. Link-local and cloud metadata addresses never go through this Mac; the phone loads them itself." } }, - "pt-BR": { + "de": { "stringUnit": { "state": "translated", - "value": "O cmux encaminha portas sem o Cloud VPN. Navegadores e outros apps do Mac precisam do Cloud VPN para acessar serviços vinculados ao endereço privado da máquina." + "value": "Der iOS-Browser kann LAN-, VPN- und Internet-Hosts über diesen Mac laden. Link-Local- und Cloud-Metadatenadressen laufen nie über diesen Mac; das iPhone lädt sie selbst." } }, - "ru": { + "fr": { "stringUnit": { "state": "translated", - "value": "cmux перенаправляет порты без Cloud VPN. Веб-браузерам и другим приложениям Mac нужен Cloud VPN, чтобы обращаться к службам, привязанным к частному адресу машины." + "value": "Le navigateur iOS peut charger des hôtes du réseau local, du VPN et d’Internet via ce Mac. Les adresses link-local et de métadonnées cloud ne passent jamais par ce Mac ; l’iPhone les charge lui-même." } }, - "th": { + "ar": { "stringUnit": { "state": "translated", - "value": "cmux ส่งต่อพอร์ตได้โดยไม่ต้องใช้ Cloud VPN เว็บเบราว์เซอร์และแอป Mac อื่นๆ ต้องใช้ Cloud VPN เพื่อเข้าถึงบริการที่ผูกกับที่อยู่ส่วนตัวของเครื่อง" + "value": "يمكن لمتصفح iOS تحميل مضيفي الشبكة المحلية وVPN والإنترنت عبر جهاز Mac هذا. لا تمر عناوين الارتباط المحلي وعناوين بيانات التعريف السحابية عبر جهاز Mac هذا أبدًا؛ يحمّلها iPhone بنفسه." } }, - "tr": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux, portları Cloud VPN olmadan yönlendirir. Web tarayıcıları ve diğer Mac uygulamaları, makinenin özel adresine bağlı hizmetlere ulaşmak için Cloud VPN’e ihtiyaç duyar." + "value": "El navegador de iOS puede cargar hosts de la red local, VPN e internet a través de este Mac. Las direcciones link-local y de metadatos en la nube nunca pasan por este Mac; el iPhone las carga por sí mismo." } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux перенаправляє порти без Cloud VPN. Веббраузерам та іншим застосункам Mac потрібен Cloud VPN, щоб звертатися до служб, прив’язаних до приватної адреси машини." + "value": "iOS 瀏覽器可透過這台 Mac 載入區域網路、VPN 與網際網路主機。連結本機位址與雲端中繼資料位址絕不經過這台 Mac,由 iPhone 自行載入。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "iOS 浏览器可通过这台 Mac 加载局域网、VPN 和互联网主机。链路本地地址和云元数据地址绝不经过这台 Mac,由 iPhone 自行加载。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "iOS 브라우저가 이 Mac을 통해 LAN, VPN, 인터넷 호스트를 불러올 수 있습니다. 링크 로컬 및 클라우드 메타데이터 주소는 이 Mac을 거치지 않으며 iPhone이 직접 불러옵니다." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "iOSブラウザはこのMac経由でLAN、VPN、インターネット上のホストを読み込めます。リンクローカルとクラウドメタデータのアドレスはこのMacを経由せず、iPhoneが直接読み込みます。" } } } }, - "cloudTree.ports.setupVPN": { + "settings.mobile.browserTunnel.allowOtherHosts.subtitleOff": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Set Up VPN…" + "value": "The iOS browser reaches only this Mac's localhost through this Mac. Other sites load over the phone's own network." } }, "de": { "stringUnit": { "state": "translated", - "value": "VPN einrichten…" + "value": "Der iOS-Browser erreicht über diesen Mac nur dessen localhost. Andere Websites werden über das eigene Netzwerk des Telefons geladen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Configurer le VPN…" + "value": "Via ce Mac, le navigateur iOS n’accède qu’au localhost de ce Mac. Les autres sites se chargent via le réseau du téléphone." } }, "ar": { "stringUnit": { "state": "translated", - "value": "إعداد VPN…" + "value": "لا يصل متصفح iOS عبر جهاز Mac هذا إلا إلى localhost الخاص به. تُحمَّل المواقع الأخرى عبر شبكة الهاتف نفسه." } }, "es": { "stringUnit": { "state": "translated", - "value": "Configurar VPN…" + "value": "A través de este Mac, el navegador de iOS solo accede a su localhost. Los demás sitios se cargan con la red del propio teléfono." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "設定 VPN…" + "value": "iOS 瀏覽器只會透過這台 Mac 連線至它的 localhost。其他網站會透過手機本身的網路載入。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "设置 VPN…" + "value": "iOS 浏览器只会通过这台 Mac 访问它的 localhost。其他网站通过手机自身的网络加载。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "VPN 설정…" + "value": "iOS 브라우저는 이 Mac을 통해 이 Mac의 localhost에만 접근합니다. 다른 사이트는 휴대폰 자체 네트워크로 불러옵니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "VPN を設定…" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Postavi VPN…" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Konfigurer VPN…" + "value": "iOSブラウザがこのMac経由で接続するのは、このMacのlocalhostだけです。他のサイトは電話自身のネットワークで読み込みます。" } - }, - "it": { + } + } + }, + "settings.search.alias.setting.mobile.browserTunnelAllowOtherHosts": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Configura VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security" } }, - "km": { + "de": { "stringUnit": { "state": "translated", - "value": "រៀបចំ VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security mobil browser tunnel proxy lokales netzwerk hosts netzwerk sicherheit" } }, - "nb": { + "fr": { "stringUnit": { "state": "translated", - "value": "Konfigurer VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security mobile navigateur tunnel proxy réseau local hôtes réseau sécurité" } }, - "pl": { + "ar": { "stringUnit": { "state": "translated", - "value": "Skonfiguruj VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security متصفح نفق وكيل شبكة محلية مضيفون شبكة أمان" } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Configurar VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security móvil navegador túnel proxy red local hosts red seguridad" } }, - "ru": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Настроить VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security 行動裝置 瀏覽器 通道 代理 區域網路 主機 網路 安全性" } }, - "th": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ตั้งค่า VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security 移动 浏览器 隧道 代理 局域网 主机 网络 安全" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "VPN’i Kur…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security 모바일 브라우저 터널 프록시 로컬 네트워크 호스트 네트워크 보안" } }, - "uk": { + "ja": { "stringUnit": { "state": "translated", - "value": "Налаштувати VPN…" + "value": "mobile ios iphone browser on iphone tunnel proxy localhost lan vpn internet hosts network security モバイル ブラウザ トンネル プロキシ ローカルネットワーク ホスト ネットワーク セキュリティ" } } } }, - "cloudTree.ports.stale": { + "cloud.vpn.setup.heading": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Port list may be out of date" + "value": "Use Cloud machines from any app" } }, "de": { "stringUnit": { "state": "translated", - "value": "Portliste möglicherweise veraltet" + "value": "Cloud-Maschinen aus jeder App verwenden" } }, "fr": { "stringUnit": { "state": "translated", - "value": "La liste des ports peut être obsolète" + "value": "Utiliser les machines Cloud depuis toute app" } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "قد تكون قائمة المنافذ قديمة" + "value": "Usar máquinas Cloud desde cualquier app" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "La lista de puertos puede estar desactualizada" + "value": "ほかのアプリからCloudマシンを使う" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "連接埠清單可能已過時" + "value": "从任何应用使用 Cloud 机器" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "端口列表可能已过期" + "value": "從任何 App 使用 Cloud 機器" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포트 목록이 오래되었을 수 있음" + "value": "모든 앱에서 Cloud 머신 사용" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "ポートリストが古い可能性があります" + "value": "استخدام أجهزة Cloud من أي تطبيق" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Lista portova je možda zastarjela" + "value": "Koristite Cloud mašine iz bilo koje aplikacije" } }, "da": { "stringUnit": { "state": "translated", - "value": "Portlisten er muligvis forældet" + "value": "Brug Cloud-maskiner fra alle apps" } }, "it": { "stringUnit": { "state": "translated", - "value": "L’elenco delle porte potrebbe non essere aggiornato" + "value": "Usa le macchine Cloud da qualsiasi app" } }, "km": { "stringUnit": { "state": "translated", - "value": "បញ្ជីច្រកអាចហួសសម័យ" + "value": "ប្រើម៉ាស៊ីន Cloud ពីកម្មវិធីណាមួយ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Portlisten kan være utdatert" + "value": "Bruk Cloud-maskiner fra alle apper" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Lista portów może być nieaktualna" + "value": "Używaj maszyn Cloud z dowolnej aplikacji" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A lista de portas pode estar desatualizada" + "value": "Use máquinas Cloud em qualquer app" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Список портов может быть устаревшим" + "value": "Используйте Cloud-машины из любого приложения" } }, "th": { "stringUnit": { "state": "translated", - "value": "รายการพอร์ตอาจไม่เป็นปัจจุบัน" + "value": "ใช้เครื่อง Cloud จากทุกแอป" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Port listesi güncel olmayabilir" + "value": "Cloud makinelerini her uygulamadan kullanın" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Список портів може бути застарілим" + "value": "Використовуйте Cloud-машини з будь-якої програми" } } } }, - "cloudTree.ports.stale.detail": { + "cloud.vpn.setup.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The last scan could not be refreshed. Listed services may have changed; refresh to check again. cmux forwarding does not need Cloud VPN." + "value": "Optional access to each VM's private address and original ports, such as 10.40.0.10:3000." } }, "de": { "stringUnit": { "state": "translated", - "value": "Der letzte Scan konnte nicht aktualisiert werden. Die aufgeführten Dienste könnten sich geändert haben. Aktualisieren Sie erneut. Die cmux-Weiterleitung benötigt kein Cloud-VPN." + "value": "Optionaler Zugriff auf die private Adresse und die ursprünglichen Ports jeder VM, etwa 10.40.0.10:3000." } }, "fr": { "stringUnit": { "state": "translated", - "value": "La dernière analyse n’a pas pu être actualisée. Les services listés peuvent avoir changé ; actualisez pour vérifier. Le transfert cmux ne nécessite pas de VPN Cloud." + "value": "Accès facultatif à chaque adresse privée et aux ports d’origine des VM, comme 10.40.0.10:3000." } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "تعذر تحديث آخر فحص. ربما تغيرت الخدمات المدرجة؛ حدّث للفحص مجددًا. لا تحتاج إعادة توجيه cmux إلى Cloud VPN." + "value": "Acceso opcional a la dirección privada y los puertos originales de cada VM, como 10.40.0.10:3000." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "No se pudo actualizar el último escaneo. Los servicios listados pueden haber cambiado; actualiza para comprobarlos. El reenvío de cmux no necesita Cloud VPN." + "value": "各VMのプライベートアドレスと元のポート(例:10.40.0.10:3000)にアクセスするための任意の設定です。" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "無法重新整理上次掃描結果。列出的服務可能已變更;請重新整理以再次檢查。cmux 轉送不需要 Cloud VPN。" + "value": "可选设置,用于访问每台 VM 的私有地址和原始端口,例如 10.40.0.10:3000。" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "无法刷新上次扫描结果。列出的服务可能已更改;请刷新以再次检查。cmux 转发无需 Cloud VPN。" + "value": "選用設定,可存取每台 VM 的私有位址和原始連接埠,例如 10.40.0.10:3000。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "마지막 검색 결과를 새로 고치지 못했습니다. 나열된 서비스가 변경되었을 수 있으니 새로 고쳐 확인하세요. cmux 전달에는 Cloud VPN이 필요하지 않습니다." + "value": "각 VM의 비공개 주소와 원래 포트(예: 10.40.0.10:3000)에 접근하기 위한 선택 설정입니다." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "前回のスキャンを更新できませんでした。表示されたサービスが変わっている可能性があります。更新して再確認してください。cmux の転送に Cloud VPN は不要です。" + "value": "وصول اختياري إلى العنوان الخاص والمنافذ الأصلية لكل جهاز افتراضي، مثل 10.40.0.10:3000." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Posljednje skeniranje nije se moglo osvježiti. Navedeni servisi su se možda promijenili; osvježite da ponovo provjerite. Prosljeđivanje u cmux-u ne treba Cloud VPN." + "value": "Opcionalni pristup privatnoj adresi i izvornim portovima svake VM, na primjer 10.40.0.10:3000." } }, "da": { "stringUnit": { "state": "translated", - "value": "Den seneste scanning kunne ikke opdateres. De viste tjenester kan have ændret sig; opdater for at kontrollere igen. Videresendelse i cmux kræver ikke Cloud VPN." + "value": "Valgfri adgang til hver VM’s private adresse og oprindelige porte, f.eks. 10.40.0.10:3000." } }, "it": { "stringUnit": { "state": "translated", - "value": "Non è stato possibile aggiornare l’ultima scansione. I servizi elencati potrebbero essere cambiati; aggiorna per controllare di nuovo. L’inoltro di cmux non richiede la VPN Cloud." + "value": "Accesso facoltativo all’indirizzo privato e alle porte originali di ogni VM, ad esempio 10.40.0.10:3000." } }, "km": { "stringUnit": { "state": "translated", - "value": "មិនអាចផ្ទុកការស្កេនចុងក្រោយឡើងវិញបានទេ។ សេវាកម្មដែលបានរាយអាចបានផ្លាស់ប្តូរ; ផ្ទុកឡើងវិញដើម្បីពិនិត្យម្តងទៀត។ ការបញ្ជូនបន្តរបស់ cmux មិនត្រូវការ Cloud VPN ទេ។" + "value": "ចូលប្រើអាសយដ្ឋានឯកជន និងច្រកដើមរបស់ VM នីមួយៗ ដូចជា 10.40.0.10:3000 ជាជម្រើស។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Den siste skanningen kunne ikke oppdateres. De oppførte tjenestene kan ha endret seg; oppdater for å sjekke på nytt. Videresending i cmux trenger ikke Cloud VPN." + "value": "Valgfri tilgang til hver VMs private adresse og opprinnelige porter, som 10.40.0.10:3000." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Nie udało się odświeżyć ostatniego skanowania. Wymienione usługi mogły się zmienić; odśwież, aby sprawdzić ponownie. Przekierowywanie w cmux nie wymaga Cloud VPN." + "value": "Opcjonalny dostęp do prywatnego adresu i oryginalnych portów każdej maszyny VM, na przykład 10.40.0.10:3000." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Não foi possível atualizar a última verificação. Os serviços listados podem ter mudado; atualize para verificar de novo. O encaminhamento do cmux não precisa do Cloud VPN." + "value": "Acesso opcional ao endereço privado e às portas originais de cada VM, como 10.40.0.10:3000." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Не удалось обновить последнее сканирование. Перечисленные службы могли измениться; обновите, чтобы проверить снова. Перенаправлению cmux Cloud VPN не нужен." + "value": "Необязательный доступ к частному адресу и исходным портам каждой VM, например 10.40.0.10:3000." } }, "th": { "stringUnit": { "state": "translated", - "value": "ไม่สามารถรีเฟรชการสแกนครั้งล่าสุดได้ บริการที่แสดงอาจเปลี่ยนไปแล้ว รีเฟรชเพื่อตรวจสอบอีกครั้ง การส่งต่อของ cmux ไม่ต้องใช้ Cloud VPN" + "value": "การเข้าถึงที่ไม่บังคับไปยังที่อยู่ส่วนตัวและพอร์ตเดิมของ VM แต่ละเครื่อง เช่น 10.40.0.10:3000" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Son tarama yenilenemedi. Listelenen hizmetler değişmiş olabilir; yeniden denetlemek için yenileyin. cmux yönlendirmesi Cloud VPN gerektirmez." + "value": "Her VM’nin özel adresine ve özgün bağlantı noktalarına isteğe bağlı erişim; örneğin 10.40.0.10:3000." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Не вдалося оновити останнє сканування. Перелічені служби могли змінитися; оновіть, щоб перевірити знову. Перенаправлення cmux не потребує Cloud VPN." + "value": "Необов’язковий доступ до приватної адреси та початкових портів кожної VM, наприклад 10.40.0.10:3000." } } } }, - "cloudTree.ports.unavailable.detail": { + "cloud.vpn.setup.howItWorks.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "The port scan failed; this does not mean no service is listening. Existing cmux ports may still work. Refresh to retry; Cloud VPN is not required." + "value": "How it works" } }, "de": { "stringUnit": { "state": "translated", - "value": "Der Portscan ist fehlgeschlagen; das bedeutet nicht, dass kein Dienst lauscht. Vorhandene cmux-Ports funktionieren möglicherweise weiter. Aktualisieren Sie; Cloud-VPN ist nicht erforderlich." + "value": "So funktioniert es" } }, "fr": { "stringUnit": { "state": "translated", - "value": "L’analyse des ports a échoué ; cela ne signifie pas qu’aucun service n’écoute. Les ports cmux existants peuvent encore fonctionner. Actualisez pour réessayer ; le VPN Cloud n’est pas nécessaire." + "value": "Fonctionnement" } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "فشل فحص المنافذ؛ هذا لا يعني عدم وجود خدمة تستمع. قد تظل منافذ cmux الحالية تعمل. حدّث للمحاولة مجددًا؛ Cloud VPN غير مطلوب." + "value": "Cómo funciona" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Falló el escaneo de puertos; no significa que no haya servicios en escucha. Los puertos de cmux existentes pueden seguir funcionando. Actualiza para reintentar; Cloud VPN no es necesario." + "value": "仕組み" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "連接埠掃描失敗;這不代表沒有服務正在監聽。現有 cmux 連接埠可能仍可使用。請重新整理以重試;不需要 Cloud VPN。" + "value": "工作原理" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "端口扫描失败;这并不代表没有服务正在监听。现有 cmux 端口可能仍可使用。请刷新以重试;无需 Cloud VPN。" + "value": "運作方式" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포트 검색이 실패했지만 수신 대기 서비스가 없다는 뜻은 아닙니다. 기존 cmux 포트는 계속 작동할 수 있습니다. 새로 고쳐 다시 시도하세요. Cloud VPN은 필요하지 않습니다." + "value": "작동 방식" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "ポートスキャンが失敗しましたが、サービスが待ち受けていないとは限りません。既存の cmux ポートは引き続き動作する可能性があります。更新して再試行してください。Cloud VPN は不要です。" + "value": "طريقة العمل" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Skeniranje portova nije uspjelo; to ne znači da nijedan servis ne sluša. Postojeći cmux portovi možda i dalje rade. Osvježite za ponovni pokušaj; Cloud VPN nije potreban." + "value": "Kako radi" } }, "da": { "stringUnit": { "state": "translated", - "value": "Portscanningen mislykkedes; det betyder ikke, at ingen tjeneste lytter. Eksisterende cmux-porte virker muligvis stadig. Opdater for at prøve igen; Cloud VPN er ikke påkrævet." + "value": "Sådan fungerer det" } }, "it": { "stringUnit": { "state": "translated", - "value": "La scansione delle porte non è riuscita; ciò non significa che nessun servizio sia in ascolto. Le porte cmux esistenti potrebbero funzionare ancora. Aggiorna per riprovare; la VPN Cloud non è necessaria." + "value": "Come funziona" } }, "km": { "stringUnit": { "state": "translated", - "value": "ការស្កេនច្រកបានបរាជ័យ; នេះមិនមានន័យថាគ្មានសេវាកម្មណាកំពុងស្តាប់ទេ។ ច្រក cmux ដែលមានស្រាប់អាចនៅតែដំណើរការ។ ផ្ទុកឡើងវិញដើម្បីព្យាយាមម្តងទៀត; មិនត្រូវការ Cloud VPN ទេ។" + "value": "របៀបដំណើរការ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Portskanningen mislyktes; det betyr ikke at ingen tjeneste lytter. Eksisterende cmux-porter kan fortsatt fungere. Oppdater for å prøve igjen; Cloud VPN er ikke nødvendig." + "value": "Slik fungerer det" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Skanowanie portów nie powiodło się; nie oznacza to, że żadna usługa nie nasłuchuje. Istniejące porty cmux mogą nadal działać. Odśwież, aby spróbować ponownie; Cloud VPN nie jest wymagany." + "value": "Jak to działa" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A verificação de portas falhou; isso não significa que nenhum serviço esteja em escuta. As portas existentes do cmux ainda podem funcionar. Atualize para tentar de novo; o Cloud VPN não é necessário." + "value": "Como funciona" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Сканирование портов не удалось; это не значит, что ни одна служба не прослушивает порты. Существующие порты cmux могут по-прежнему работать. Обновите, чтобы повторить; Cloud VPN не требуется." + "value": "Как это работает" } }, "th": { "stringUnit": { "state": "translated", - "value": "การสแกนพอร์ตล้มเหลว ไม่ได้หมายความว่าไม่มีบริการที่กำลังรับฟัง พอร์ต cmux ที่มีอยู่อาจยังใช้งานได้ รีเฟรชเพื่อลองอีกครั้ง ไม่จำเป็นต้องใช้ Cloud VPN" + "value": "วิธีการทำงาน" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Port taraması başarısız oldu; bu, dinleyen bir hizmet olmadığı anlamına gelmez. Mevcut cmux portları hâlâ çalışıyor olabilir. Yeniden denemek için yenileyin; Cloud VPN gerekmez." + "value": "Nasıl çalışır" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Сканування портів не вдалося; це не означає, що жодна служба не прослуховує. Наявні порти cmux можуть і далі працювати. Оновіть, щоб повторити; Cloud VPN не потрібен." + "value": "Як це працює" } } } }, - "cloudTree.ports.unsupported": { + "cloud.vpn.setup.status.off": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Ports are not supported by this provider." + "value": "Off. Your Cloud panes continue to work through cmux's built-in route." } }, "de": { "stringUnit": { "state": "translated", - "value": "Ports werden von diesem Anbieter nicht unterstützt." + "value": "Aus. Ihre Cloud-Bereiche funktionieren weiterhin über die integrierte Verbindung von cmux." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Les ports ne sont pas pris en charge par ce fournisseur." + "value": "Désactivé. Vos volets Cloud continuent de fonctionner via la connexion intégrée de cmux." } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "لا يدعم هذا الموفر المنافذ." + "value": "Desactivada. Tus paneles Cloud siguen funcionando mediante la conexión integrada de cmux." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este proveedor no admite puertos." + "value": "オフ。Cloudペインは引き続きcmux内蔵の接続経路で動作します。" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此提供者不支援連接埠。" + "value": "已关闭。Cloud 面板仍通过 cmux 内置连接工作。" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此提供商不支持端口。" + "value": "已關閉。Cloud 面板仍透過 cmux 內建連線運作。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 제공자는 포트를 지원하지 않습니다." + "value": "꺼짐. Cloud 패널은 cmux의 내장 연결을 통해 계속 작동합니다." } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このプロバイダーはポートをサポートしていません。" + "value": "متوقف. تستمر لوحات Cloud في العمل عبر اتصال cmux المضمن." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Ovaj pružalac ne podržava portove." + "value": "Isključeno. Cloud paneli i dalje rade preko ugrađene cmux veze." } }, "da": { "stringUnit": { "state": "translated", - "value": "Porte understøttes ikke af denne udbyder." + "value": "Fra. Dine Cloud-paneler fungerer stadig via cmux’ indbyggede forbindelse." } }, "it": { "stringUnit": { "state": "translated", - "value": "Le porte non sono supportate da questo provider." + "value": "Disattivata. I pannelli Cloud continuano a funzionare tramite la connessione integrata di cmux." } }, "km": { "stringUnit": { "state": "translated", - "value": "អ្នកផ្តល់សេវានេះមិនគាំទ្រច្រកទេ។" + "value": "បិទ។ ផ្ទាំង Cloud របស់អ្នកនៅតែដំណើរការតាមការភ្ជាប់ដែលមានស្រាប់របស់ cmux។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Porter støttes ikke av denne leverandøren." + "value": "Av. Cloud-panelene dine fortsetter å fungere gjennom cmux sin innebygde forbindelse." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Ten dostawca nie obsługuje portów." + "value": "Wyłączona. Panele Cloud nadal działają przez wbudowane połączenie cmux." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Este provedor não oferece suporte a portas." + "value": "Desativada. Seus painéis Cloud continuam funcionando pela conexão integrada do cmux." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Этот провайдер не поддерживает порты." + "value": "Выключено. Панели Cloud продолжают работать через встроенное соединение cmux." } }, "th": { "stringUnit": { "state": "translated", - "value": "ผู้ให้บริการนี้ไม่รองรับพอร์ต" + "value": "ปิดอยู่ แผง Cloud ยังคงทำงานผ่านการเชื่อมต่อในตัวของ cmux" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu sağlayıcı portları desteklemiyor." + "value": "Kapalı. Cloud panelleriniz cmux’un yerleşik bağlantısıyla çalışmaya devam eder." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Цей постачальник не підтримує порти." + "value": "Вимкнено. Панелі Cloud продовжують працювати через вбудоване з’єднання cmux." } } } }, - "cloudTree.ports.unsupported.detail": { + "cloud.vpn.setup.steps.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "This provider has no supported in-app port route. Open a shell to access the service inside the machine. Cloud VPN does not add provider support." + "value": "First-time setup" } }, "de": { "stringUnit": { "state": "translated", - "value": "Dieser Anbieter unterstützt keine Portverbindung in der App. Öffnen Sie eine Shell, um innerhalb der Maschine auf den Dienst zuzugreifen. Cloud-VPN fügt keine Anbieterunterstützung hinzu." + "value": "Ersteinrichtung" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Ce fournisseur ne prend pas en charge les ports dans l’app. Ouvrez un shell pour accéder au service dans la machine. Le VPN Cloud n’ajoute pas cette prise en charge." + "value": "Première configuration" } }, - "ar": { + "es": { "stringUnit": { "state": "translated", - "value": "لا يدعم هذا الموفر مسار منافذ داخل التطبيق. افتح صدفة للوصول إلى الخدمة داخل الجهاز. لا يضيف Cloud VPN دعمًا للموفر." + "value": "Configuración inicial" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Este proveedor no admite una ruta de puertos dentro de la app. Abre una shell para acceder al servicio dentro de la máquina. Cloud VPN no añade soporte al proveedor." + "value": "初回設定" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "此提供者不支援應用程式內連接埠路徑。請開啟 Shell 以在機器內存取服務。Cloud VPN 不會新增提供者支援。" + "value": "首次设置" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "此提供商不支持应用内端口路由。请打开 Shell 以在机器内部访问服务。Cloud VPN 不会添加提供商支持。" + "value": "首次設定" } }, "ko": { "stringUnit": { "state": "translated", - "value": "이 제공자는 앱 내 포트 경로를 지원하지 않습니다. 셸을 열어 머신 내부의 서비스에 접근하세요. Cloud VPN으로 제공자 지원이 추가되지는 않습니다." + "value": "최초 설정" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "このプロバイダーはアプリ内のポート経路に対応していません。シェルを開き、マシン内でサービスにアクセスしてください。Cloud VPN を有効にしても対応は追加されません。" + "value": "الإعداد الأولي" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Ovaj pružalac nema podržanu rutu portova unutar aplikacije. Otvorite shell da pristupite servisu unutar mašine. Cloud VPN ne dodaje podršku pružaoca." + "value": "Prvo podešavanje" } }, "da": { "stringUnit": { "state": "translated", - "value": "Denne udbyder har ingen understøttet portrute i appen. Åbn en shell for at få adgang til tjenesten inde i maskinen. Cloud VPN tilføjer ikke understøttelse for udbyderen." + "value": "Første opsætning" } }, "it": { "stringUnit": { "state": "translated", - "value": "Questo provider non ha un percorso delle porte supportato nell’app. Apri una shell per accedere al servizio all’interno della macchina. La VPN Cloud non aggiunge il supporto del provider." + "value": "Prima configurazione" } }, "km": { "stringUnit": { "state": "translated", - "value": "អ្នកផ្តល់សេវានេះគ្មានផ្លូវច្រកក្នុងកម្មវិធីដែលគាំទ្រទេ។ បើក shell ដើម្បីចូលប្រើសេវាកម្មនៅខាងក្នុងម៉ាស៊ីន។ Cloud VPN មិនបន្ថែមការគាំទ្រអ្នកផ្តល់សេវាទេ។" + "value": "ការរៀបចំលើកដំបូង" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Denne leverandøren har ingen støttet portrute i appen. Åpne et skall for å få tilgang til tjenesten inne i maskinen. Cloud VPN legger ikke til støtte for leverandøren." + "value": "Førstegangsoppsett" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Ten dostawca nie ma obsługiwanej trasy portów w aplikacji. Otwórz powłokę, aby uzyskać dostęp do usługi wewnątrz maszyny. Cloud VPN nie dodaje obsługi dostawcy." + "value": "Pierwsza konfiguracja" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Este provedor não tem uma rota de portas compatível no app. Abra um shell para acessar o serviço dentro da máquina. O Cloud VPN não adiciona suporte ao provedor." + "value": "Primeira configuração" } }, "ru": { "stringUnit": { "state": "translated", - "value": "У этого провайдера нет поддерживаемого маршрута портов в приложении. Откройте оболочку, чтобы получить доступ к службе внутри машины. Cloud VPN не добавляет поддержку провайдера." + "value": "Первая настройка" } }, "th": { "stringUnit": { "state": "translated", - "value": "ผู้ให้บริการนี้ไม่มีเส้นทางพอร์ตในแอปที่รองรับ เปิดเชลล์เพื่อเข้าถึงบริการภายในเครื่อง Cloud VPN ไม่ได้เพิ่มการรองรับผู้ให้บริการ" + "value": "การตั้งค่าครั้งแรก" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Bu sağlayıcının desteklenen bir uygulama içi port yolu yok. Makinenin içindeki hizmete erişmek için bir kabuk açın. Cloud VPN sağlayıcı desteği eklemez." + "value": "İlk kurulum" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Цей постачальник не має підтримуваного маршруту портів у застосунку. Відкрийте оболонку, щоб отримати доступ до служби всередині машини. Cloud VPN не додає підтримку постачальника." + "value": "Перше налаштування" } } } }, - "cloudTree.port.openInCmux": { + "cloud.vpn.setup.steps.extension": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Open in cmux" + "value": "1. Click Connect Cloud VPN. When macOS asks, allow the cmux network extension in System Settings." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "فتح في cmux" + "value": "1. Klicken Sie auf Cloud-VPN verbinden. Erlauben Sie auf Aufforderung von macOS die cmux-Netzwerkerweiterung in den Systemeinstellungen." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "In cmux öffnen" + "value": "1. Cliquez sur Connecter le VPN Cloud. Lorsque macOS le demande, autorisez l’extension réseau cmux dans Réglages Système." } }, "es": { "stringUnit": { "state": "translated", - "value": "Abrir en cmux" + "value": "1. Haz clic en Conectar VPN de Cloud. Cuando macOS lo solicite, permite la extensión de red de cmux en Ajustes del Sistema." } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Ouvrir dans cmux" + "value": "1. 「Cloud VPNに接続」をクリックします。macOSに求められたら、システム設定でcmuxのネットワーク機能拡張を許可します。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmuxで開く" + "value": "1. 点击“连接 Cloud VPN”。macOS 提示时,在系统设置中允许 cmux 网络扩展。" } }, - "ko": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "cmux에서 열기" + "value": "1. 按一下「連接 Cloud VPN」。macOS 提示時,請在系統設定中允許 cmux 網路延伸功能。" } }, - "zh-Hans": { + "ko": { "stringUnit": { "state": "translated", - "value": "在 cmux 中打开" + "value": "1. Cloud VPN 연결을 클릭합니다. macOS에서 요청하면 시스템 설정에서 cmux 네트워크 확장을 허용합니다." } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "在 cmux 中開啟" + "value": "1. انقر على توصيل VPN الخاص بـ Cloud. عندما يطلب macOS ذلك، اسمح بامتداد شبكة cmux في إعدادات النظام." } - } - } - }, - "cloudTree.port.openInCmux.help": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "bs": { "stringUnit": { "state": "translated", - "value": "Open in cmux. No VPN setup needed." + "value": "1. Kliknite Poveži Cloud VPN. Kada macOS zatraži, dozvolite cmux mrežni dodatak u Postavkama sistema." } }, - "ar": { + "da": { "stringUnit": { "state": "translated", - "value": "افتح في cmux. لا يلزم إعداد VPN." + "value": "1. Klik på Forbind Cloud VPN. Når macOS spørger, skal du tillade cmux-netværksudvidelsen i Systemindstillinger." } }, - "de": { + "it": { "stringUnit": { "state": "translated", - "value": "In cmux öffnen. Keine VPN-Einrichtung nötig." + "value": "1. Fai clic su Connetti VPN Cloud. Quando macOS lo richiede, consenti l’estensione di rete cmux nelle Impostazioni di Sistema." } }, - "es": { + "km": { "stringUnit": { "state": "translated", - "value": "Abrir en cmux. No hace falta configurar una VPN." + "value": "1. ចុច ភ្ជាប់ Cloud VPN។ ពេល macOS ស្នើ សូមអនុញ្ញាតផ្នែកបន្ថែមបណ្តាញ cmux ក្នុងការកំណត់ប្រព័ន្ធ។" } }, - "fr": { + "nb": { "stringUnit": { "state": "translated", - "value": "Ouvrir dans cmux. Aucune configuration VPN nécessaire." + "value": "1. Klikk Koble til Cloud VPN. Når macOS spør, tillat cmux-nettverksutvidelsen i Systeminnstillinger." } }, - "ja": { + "pl": { "stringUnit": { "state": "translated", - "value": "cmux で開きます。VPN の設定は不要です。" + "value": "1. Kliknij Połącz z Cloud VPN. Gdy macOS poprosi, zezwól na rozszerzenie sieci cmux w Ustawieniach systemowych." } }, - "ko": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "cmux에서 열기. VPN 설정이 필요하지 않습니다." + "value": "1. Clique em Conectar Cloud VPN. Quando o macOS pedir, permita a extensão de rede do cmux nos Ajustes do Sistema." } }, - "zh-Hans": { + "ru": { "stringUnit": { "state": "translated", - "value": "在 cmux 中打开。无需设置 VPN。" + "value": "1. Нажмите Подключить Cloud VPN. Когда macOS попросит, разрешите сетевое расширение cmux в системных настройках." } }, - "zh-Hant": { + "th": { "stringUnit": { "state": "translated", - "value": "在 cmux 中開啟。無需設定 VPN。" + "value": "1. คลิก เชื่อมต่อ Cloud VPN เมื่อ macOS ขอ ให้อนุญาตส่วนขยายเครือข่าย cmux ในการตั้งค่าระบบ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "1. Cloud VPN’e bağlan’a tıklayın. macOS istediğinde Sistem Ayarları’nda cmux ağ uzantısına izin verin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "1. Натисніть Підключити Cloud VPN. Коли macOS попросить, дозвольте мережеве розширення cmux у Системних параметрах." } } } }, - "cloudTree.port.title": { + "cloud.vpn.setup.steps.settings": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Port %@" + "value": "Open General > Login Items & Extensions > Network Extensions, then enable cmux." } }, - "ar": { + "de": { "stringUnit": { "state": "translated", - "value": "المنفذ %@" + "value": "Öffnen Sie Allgemein > Anmeldeobjekte & Erweiterungen > Netzwerkerweiterungen und aktivieren Sie cmux." } }, - "de": { + "fr": { "stringUnit": { "state": "translated", - "value": "Port-Nr. %@" + "value": "Ouvrez Général > Ouverture et extensions > Extensions réseau, puis activez cmux." } }, "es": { "stringUnit": { "state": "translated", - "value": "Puerto %@" + "value": "Abre General > Ítems de inicio y extensiones > Extensiones de red y activa cmux." } }, - "fr": { + "ja": { "stringUnit": { "state": "translated", - "value": "Port n° %@" + "value": "「一般」>「ログイン項目と機能拡張」>「ネットワーク機能拡張」を開き、cmuxを有効にします。" } }, - "ja": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "ポート %@" + "value": "打开“通用”>“登录项与扩展”>“网络扩展”,然后启用 cmux。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "打開「一般」>「登入項目與延伸功能」>「網路延伸功能」,然後啟用 cmux。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "포트 %@" + "value": "일반 > 로그인 항목 및 확장 프로그램 > 네트워크 확장을 열고 cmux를 활성화합니다." } }, - "zh-Hans": { + "ar": { "stringUnit": { "state": "translated", - "value": "端口 %@" + "value": "افتح عام > عناصر الدخول والامتدادات > امتدادات الشبكة، ثم فعّل cmux." } }, - "zh-Hant": { + "bs": { "stringUnit": { "state": "translated", - "value": "連接埠 %@" + "value": "Otvorite Opšte > Stavke za prijavu i dodaci > Mrežni dodaci, pa omogućite cmux." } - } - } - }, - "cloudTree.placeholder.deviceOffline": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "da": { "stringUnit": { "state": "translated", - "value": "Offline — its workspaces return when it does" + "value": "Åbn Generelt > Loginemner og udvidelser > Netværksudvidelser, og aktivér cmux." } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "غير متصل — ستعود مساحات عمله عند اتصاله مجددًا" + "value": "Apri Generali > Elementi login ed estensioni > Estensioni di rete, quindi abilita cmux." } }, - "de": { + "km": { "stringUnit": { "state": "translated", - "value": "Offline — die Arbeitsbereiche erscheinen wieder, sobald das Gerät online ist" + "value": "បើក ទូទៅ > ធាតុចូល និងផ្នែកបន្ថែម > ផ្នែកបន្ថែមបណ្តាញ ហើយបើក cmux។" } }, - "es": { + "nb": { "stringUnit": { "state": "translated", - "value": "Sin conexión — sus espacios de trabajo volverán cuando se conecte" + "value": "Åpne Generelt > Påloggingselementer og utvidelser > Nettverksutvidelser, og aktiver cmux." } }, - "fr": { + "pl": { "stringUnit": { "state": "translated", - "value": "Hors ligne — ses espaces de travail réapparaîtront à sa reconnexion" + "value": "Otwórz Ogólne > Elementy logowania i rozszerzenia > Rozszerzenia sieciowe, a następnie włącz cmux." } }, - "ja": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "オフライン — 再接続するとワークスペースが戻ります" + "value": "Abra Geral > Itens de Início e Extensões > Extensões de Rede e ative o cmux." } }, - "ko": { + "ru": { "stringUnit": { "state": "translated", - "value": "오프라인 — 다시 연결되면 작업 공간이 돌아옵니다" + "value": "Откройте Основные > Объекты входа и расширения > Сетевые расширения и включите cmux." } }, - "zh-Hans": { + "th": { "stringUnit": { "state": "translated", - "value": "离线 — 重新连接后会恢复工作区" + "value": "เปิด ทั่วไป > รายการเข้าสู่ระบบและส่วนขยาย > ส่วนขยายเครือข่าย แล้วเปิดใช้ cmux" } }, - "zh-Hant": { + "tr": { "stringUnit": { "state": "translated", - "value": "離線 — 重新連線後會恢復工作區" + "value": "Genel > Giriş Öğeleri ve Uzantılar > Ağ Uzantıları’nı açın ve cmux’u etkinleştirin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Відкрийте Загальні > Об’єкти входу та розширення > Мережеві розширення й увімкніть cmux." } } } }, - "cloud.vpn.setup.heading": { + "cloud.vpn.setup.steps.settingsLegacy": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Use Cloud machines from any app" + "value": "Open Privacy & Security and allow the cmux extension." } }, "de": { "stringUnit": { "state": "translated", - "value": "Cloud-Maschinen aus jeder App verwenden" + "value": "Öffnen Sie Datenschutz & Sicherheit und erlauben Sie die cmux-Erweiterung." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Utiliser les machines Cloud depuis toute app" + "value": "Ouvrez Confidentialité et sécurité et autorisez l’extension cmux." } }, "es": { "stringUnit": { "state": "translated", - "value": "Usar máquinas Cloud desde cualquier app" + "value": "Abre Privacidad y seguridad y permite la extensión de cmux." } }, "ja": { "stringUnit": { "state": "translated", - "value": "ほかのアプリからCloudマシンを使う" + "value": "「プライバシーとセキュリティ」を開き、cmuxの機能拡張を許可します。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "从任何应用使用 Cloud 机器" + "value": "打开“隐私与安全性”,然后允许 cmux 扩展。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "從任何 App 使用 Cloud 機器" + "value": "打開「隱私權與安全性」,然後允許 cmux 延伸功能。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "모든 앱에서 Cloud 머신 사용" + "value": "개인정보 보호 및 보안을 열고 cmux 확장을 허용합니다." } }, "ar": { "stringUnit": { "state": "translated", - "value": "استخدام أجهزة Cloud من أي تطبيق" + "value": "افتح الخصوصية والأمان واسمح بامتداد cmux." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Koristite Cloud mašine iz bilo koje aplikacije" + "value": "Otvorite Privatnost i sigurnost i dozvolite cmux dodatak." } }, "da": { "stringUnit": { "state": "translated", - "value": "Brug Cloud-maskiner fra alle apps" + "value": "Åbn Anonymitet og sikkerhed, og tillad cmux-udvidelsen." } }, "it": { "stringUnit": { "state": "translated", - "value": "Usa le macchine Cloud da qualsiasi app" + "value": "Apri Privacy e sicurezza e consenti l’estensione cmux." } }, "km": { "stringUnit": { "state": "translated", - "value": "ប្រើម៉ាស៊ីន Cloud ពីកម្មវិធីណាមួយ" + "value": "បើក ឯកជនភាព និងសុវត្ថិភាព ហើយអនុញ្ញាតផ្នែកបន្ថែម cmux។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Bruk Cloud-maskiner fra alle apper" + "value": "Åpne Personvern og sikkerhet og tillat cmux-utvidelsen." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Używaj maszyn Cloud z dowolnej aplikacji" + "value": "Otwórz Prywatność i ochrona i zezwól na rozszerzenie cmux." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Use máquinas Cloud em qualquer app" + "value": "Abra Privacidade e Segurança e permita a extensão do cmux." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Используйте Cloud-машины из любого приложения" + "value": "Откройте Конфиденциальность и безопасность и разрешите расширение cmux." } }, "th": { "stringUnit": { "state": "translated", - "value": "ใช้เครื่อง Cloud จากทุกแอป" + "value": "เปิด ความเป็นส่วนตัวและความปลอดภัย แล้วอนุญาตส่วนขยาย cmux" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Cloud makinelerini her uygulamadan kullanın" + "value": "Gizlilik ve Güvenlik’i açın ve cmux uzantısına izin verin." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Використовуйте Cloud-машини з будь-якої програми" + "value": "Відкрийте Конфіденційність і безпека та дозвольте розширення cmux." } } } }, - "cloud.vpn.setup.subtitle": { + "cloud.vpn.setup.steps.configuration": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Optional access to each VM's private address and original ports, such as 10.40.0.10:3000." + "value": "2. Allow cmux to add a VPN configuration named cmux Cloud. This is a separate macOS permission." } }, "de": { "stringUnit": { "state": "translated", - "value": "Optionaler Zugriff auf die private Adresse und die ursprünglichen Ports jeder VM, etwa 10.40.0.10:3000." + "value": "2. Erlauben Sie cmux, eine VPN-Konfiguration namens cmux Cloud hinzuzufügen. Dies ist eine separate macOS-Berechtigung." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Accès facultatif à chaque adresse privée et aux ports d’origine des VM, comme 10.40.0.10:3000." + "value": "2. Autorisez cmux à ajouter une configuration VPN nommée cmux Cloud. Il s’agit d’une autorisation macOS distincte." } }, "es": { "stringUnit": { "state": "translated", - "value": "Acceso opcional a la dirección privada y los puertos originales de cada VM, como 10.40.0.10:3000." + "value": "2. Permite que cmux añada una configuración VPN llamada cmux Cloud. Este es un permiso independiente de macOS." } }, "ja": { "stringUnit": { "state": "translated", - "value": "各VMのプライベートアドレスと元のポート(例:10.40.0.10:3000)にアクセスするための任意の設定です。" + "value": "2. cmux Cloudという名前のVPN構成の追加を許可します。これは別のmacOS権限です。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "可选设置,用于访问每台 VM 的私有地址和原始端口,例如 10.40.0.10:3000。" + "value": "2. 允许 cmux 添加名为 cmux Cloud 的 VPN 配置。这是单独的 macOS 权限。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "選用設定,可存取每台 VM 的私有位址和原始連接埠,例如 10.40.0.10:3000。" + "value": "2. 允許 cmux 加入名為 cmux Cloud 的 VPN 設定。這是另一項 macOS 權限。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "각 VM의 비공개 주소와 원래 포트(예: 10.40.0.10:3000)에 접근하기 위한 선택 설정입니다." + "value": "2. cmux가 cmux Cloud라는 VPN 구성을 추가하도록 허용합니다. 이는 별도의 macOS 권한입니다." } }, "ar": { "stringUnit": { "state": "translated", - "value": "وصول اختياري إلى العنوان الخاص والمنافذ الأصلية لكل جهاز افتراضي، مثل 10.40.0.10:3000." + "value": "2. اسمح لـ cmux بإضافة إعداد VPN باسم cmux Cloud. هذا إذن منفصل في macOS." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Opcionalni pristup privatnoj adresi i izvornim portovima svake VM, na primjer 10.40.0.10:3000." + "value": "2. Dozvolite cmux-u da doda VPN konfiguraciju pod imenom cmux Cloud. Ovo je posebna macOS dozvola." } }, "da": { "stringUnit": { "state": "translated", - "value": "Valgfri adgang til hver VM’s private adresse og oprindelige porte, f.eks. 10.40.0.10:3000." + "value": "2. Tillad cmux at tilføje en VPN-konfiguration med navnet cmux Cloud. Dette er en separat macOS-tilladelse." } }, "it": { "stringUnit": { "state": "translated", - "value": "Accesso facoltativo all’indirizzo privato e alle porte originali di ogni VM, ad esempio 10.40.0.10:3000." + "value": "2. Consenti a cmux di aggiungere una configurazione VPN chiamata cmux Cloud. È un’autorizzazione macOS separata." } }, "km": { "stringUnit": { "state": "translated", - "value": "ចូលប្រើអាសយដ្ឋានឯកជន និងច្រកដើមរបស់ VM នីមួយៗ ដូចជា 10.40.0.10:3000 ជាជម្រើស។" + "value": "2. អនុញ្ញាតឱ្យ cmux បន្ថែមការកំណត់ VPN ឈ្មោះ cmux Cloud។ នេះជាសិទ្ធិ macOS ផ្សេងមួយ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Valgfri tilgang til hver VMs private adresse og opprinnelige porter, som 10.40.0.10:3000." + "value": "2. Tillat cmux å legge til en VPN-konfigurasjon kalt cmux Cloud. Dette er en separat macOS-tillatelse." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Opcjonalny dostęp do prywatnego adresu i oryginalnych portów każdej maszyny VM, na przykład 10.40.0.10:3000." + "value": "2. Zezwól cmux dodać konfigurację VPN o nazwie cmux Cloud. To osobne uprawnienie macOS." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Acesso opcional ao endereço privado e às portas originais de cada VM, como 10.40.0.10:3000." + "value": "2. Permita que o cmux adicione uma configuração VPN chamada cmux Cloud. Esta é uma permissão separada do macOS." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Необязательный доступ к частному адресу и исходным портам каждой VM, например 10.40.0.10:3000." + "value": "2. Разрешите cmux добавить VPN-конфигурацию с именем cmux Cloud. Это отдельное разрешение macOS." } }, "th": { "stringUnit": { "state": "translated", - "value": "การเข้าถึงที่ไม่บังคับไปยังที่อยู่ส่วนตัวและพอร์ตเดิมของ VM แต่ละเครื่อง เช่น 10.40.0.10:3000" + "value": "2. อนุญาตให้ cmux เพิ่มการกำหนดค่า VPN ชื่อ cmux Cloud นี่เป็นสิทธิ์ macOS แยกต่างหาก" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Her VM’nin özel adresine ve özgün bağlantı noktalarına isteğe bağlı erişim; örneğin 10.40.0.10:3000." + "value": "2. cmux’un cmux Cloud adlı bir VPN yapılandırması eklemesine izin verin. Bu ayrı bir macOS iznidir." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Необов’язковий доступ до приватної адреси та початкових портів кожної VM, наприклад 10.40.0.10:3000." + "value": "2. Дозвольте cmux додати конфігурацію VPN з назвою cmux Cloud. Це окремий дозвіл macOS." } } } }, - "cloud.vpn.setup.howItWorks.title": { + "cloud.vpn.setup.steps.return": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "How it works" + "value": "3. Return to this pane. The connection continues automatically after approval. Disconnect here when finished. Quitting cmux or signing out also disconnects it." } }, "de": { "stringUnit": { "state": "translated", - "value": "So funktioniert es" + "value": "3. Kehren Sie zu diesem Bereich zurück. Die Verbindung wird nach der Genehmigung automatisch fortgesetzt. Trennen Sie sie hier, wenn Sie fertig sind. Das Beenden von cmux oder Abmelden trennt sie ebenfalls." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Fonctionnement" + "value": "3. Revenez à ce volet. La connexion reprend automatiquement après autorisation. Déconnectez-la ici lorsque vous avez terminé. Quitter cmux ou fermer la session la déconnecte aussi." } }, "es": { "stringUnit": { "state": "translated", - "value": "Cómo funciona" + "value": "3. Vuelve a este panel. La conexión continúa automáticamente tras la autorización. Desconéctala aquí cuando termines. Salir de cmux o cerrar sesión también la desconecta." } }, "ja": { "stringUnit": { "state": "translated", - "value": "仕組み" + "value": "3. このペインに戻ります。承認後、接続は自動的に続行されます。使い終わったらここで切断してください。cmuxの終了やサインアウトでも切断されます。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "工作原理" + "value": "3. 返回此面板。批准后连接会自动继续。完成后可在此断开连接。退出 cmux 或退出登录也会断开连接。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "運作方式" + "value": "3. 返回此面板。核准後連線會自動繼續。完成後可在此中斷連線。結束 cmux 或登出也會中斷連線。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "작동 방식" + "value": "3. 이 패널로 돌아옵니다. 승인 후 연결이 자동으로 계속됩니다. 사용을 마치면 여기에서 연결을 해제하세요. cmux를 종료하거나 로그아웃해도 연결이 해제됩니다." } }, "ar": { "stringUnit": { "state": "translated", - "value": "طريقة العمل" + "value": "3. ارجع إلى هذه اللوحة. يستمر الاتصال تلقائيًا بعد الموافقة. اقطع الاتصال هنا عند الانتهاء. يؤدي إنهاء cmux أو تسجيل الخروج أيضًا إلى قطع الاتصال." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Kako radi" + "value": "3. Vratite se na ovaj panel. Veza se nastavlja automatski nakon odobrenja. Ovdje prekinite vezu kada završite. Izlazak iz cmux-a ili odjava takođe prekida vezu." } }, "da": { "stringUnit": { "state": "translated", - "value": "Sådan fungerer det" + "value": "3. Gå tilbage til dette panel. Forbindelsen fortsætter automatisk efter godkendelsen. Afbryd her, når du er færdig. Hvis du afslutter cmux eller logger ud, afbrydes forbindelsen også." } }, "it": { "stringUnit": { "state": "translated", - "value": "Come funziona" + "value": "3. Torna a questo pannello. La connessione continua automaticamente dopo l’autorizzazione. Disconnettila qui quando hai finito. Anche uscire da cmux o effettuare il logout la disconnette." } }, "km": { "stringUnit": { "state": "translated", - "value": "របៀបដំណើរការ" + "value": "3. ត្រឡប់ទៅផ្ទាំងនេះ។ ការភ្ជាប់នឹងបន្តដោយស្វ័យប្រវត្តិបន្ទាប់ពីអនុម័ត។ ផ្តាច់នៅទីនេះពេលបញ្ចប់។ ការបិទ cmux ឬចាកចេញក៏ផ្តាច់ការភ្ជាប់ដែរ។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Slik fungerer det" + "value": "3. Gå tilbake til dette panelet. Forbindelsen fortsetter automatisk etter godkjenning. Koble fra her når du er ferdig. Avslutning av cmux eller utlogging kobler også fra." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Jak to działa" + "value": "3. Wróć do tego panelu. Połączenie będzie kontynuowane automatycznie po zatwierdzeniu. Po zakończeniu rozłącz je tutaj. Zamknięcie cmux lub wylogowanie również rozłączy połączenie." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Como funciona" + "value": "3. Volte a este painel. A conexão continuará automaticamente após a aprovação. Desconecte aqui quando terminar. Sair do cmux ou encerrar a sessão também desconecta." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Как это работает" + "value": "3. Вернитесь в эту панель. После подтверждения соединение продолжится автоматически. Завершив работу, отключите его здесь. Выход из cmux или системы также отключает соединение." } }, "th": { "stringUnit": { "state": "translated", - "value": "วิธีการทำงาน" + "value": "3. กลับมาที่แผงนี้ การเชื่อมต่อจะดำเนินต่อโดยอัตโนมัติหลังการอนุมัติ ตัดการเชื่อมต่อที่นี่เมื่อเสร็จแล้ว การออกจาก cmux หรือออกจากระบบจะตัดการเชื่อมต่อด้วย" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Nasıl çalışır" + "value": "3. Bu panele dönün. Onaydan sonra bağlantı otomatik olarak devam eder. Bitirdiğinizde buradan bağlantıyı kesin. cmux’tan çıkmak veya oturumu kapatmak da bağlantıyı keser." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Як це працює" + "value": "3. Поверніться до цієї панелі. Після схвалення з’єднання продовжиться автоматично. Завершивши роботу, від’єднайте його тут. Вихід із cmux або системи також від’єднає з’єднання." } } } }, - "cloud.vpn.setup.status.off": { + "cloud.vpn.setup.address.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Off. Your Cloud panes continue to work through cmux's built-in route." + "value": "Private addresses" } }, "de": { "stringUnit": { "state": "translated", - "value": "Aus. Ihre Cloud-Bereiche funktionieren weiterhin über die integrierte Verbindung von cmux." + "value": "Private Adressen" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Désactivé. Vos volets Cloud continuent de fonctionner via la connexion intégrée de cmux." + "value": "Adresses privées" } }, "es": { "stringUnit": { "state": "translated", - "value": "Desactivada. Tus paneles Cloud siguen funcionando mediante la conexión integrada de cmux." + "value": "Direcciones privadas" } }, "ja": { "stringUnit": { "state": "translated", - "value": "オフ。Cloudペインは引き続きcmux内蔵の接続経路で動作します。" + "value": "プライベートアドレス" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "已关闭。Cloud 面板仍通过 cmux 内置连接工作。" + "value": "私有地址" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "已關閉。Cloud 面板仍透過 cmux 內建連線運作。" + "value": "私有位址" } }, "ko": { "stringUnit": { "state": "translated", - "value": "꺼짐. Cloud 패널은 cmux의 내장 연결을 통해 계속 작동합니다." + "value": "비공개 주소" } }, "ar": { "stringUnit": { "state": "translated", - "value": "متوقف. تستمر لوحات Cloud في العمل عبر اتصال cmux المضمن." + "value": "العناوين الخاصة" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Isključeno. Cloud paneli i dalje rade preko ugrađene cmux veze." + "value": "Privatne adrese" } }, "da": { "stringUnit": { "state": "translated", - "value": "Fra. Dine Cloud-paneler fungerer stadig via cmux’ indbyggede forbindelse." + "value": "Private adresser" } }, "it": { "stringUnit": { "state": "translated", - "value": "Disattivata. I pannelli Cloud continuano a funzionare tramite la connessione integrata di cmux." + "value": "Indirizzi privati" } }, "km": { "stringUnit": { "state": "translated", - "value": "បិទ។ ផ្ទាំង Cloud របស់អ្នកនៅតែដំណើរការតាមការភ្ជាប់ដែលមានស្រាប់របស់ cmux។" + "value": "អាសយដ្ឋានឯកជន" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Av. Cloud-panelene dine fortsetter å fungere gjennom cmux sin innebygde forbindelse." + "value": "Private adresser" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Wyłączona. Panele Cloud nadal działają przez wbudowane połączenie cmux." + "value": "Prywatne adresy" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Desativada. Seus painéis Cloud continuam funcionando pela conexão integrada do cmux." + "value": "Endereços privados" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Выключено. Панели Cloud продолжают работать через встроенное соединение cmux." + "value": "Частные адреса" } }, "th": { "stringUnit": { "state": "translated", - "value": "ปิดอยู่ แผง Cloud ยังคงทำงานผ่านการเชื่อมต่อในตัวของ cmux" + "value": "ที่อยู่ส่วนตัว" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Kapalı. Cloud panelleriniz cmux’un yerleşik bağlantısıyla çalışmaya devam eder." + "value": "Özel adresler" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Вимкнено. Панелі Cloud продовжують працювати через вбудоване з’єднання cmux." + "value": "Приватні адреси" } } } }, - "cloud.vpn.setup.steps.title": { + "cloud.vpn.setup.openSettings": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "First-time setup" + "value": "Open System Settings" } }, "de": { "stringUnit": { "state": "translated", - "value": "Ersteinrichtung" + "value": "Systemeinstellungen öffnen" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Première configuration" + "value": "Ouvrir Réglages Système" } }, "es": { "stringUnit": { "state": "translated", - "value": "Configuración inicial" + "value": "Abrir Ajustes del Sistema" } }, "ja": { "stringUnit": { "state": "translated", - "value": "初回設定" + "value": "システム設定を開く" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "首次设置" + "value": "打开系统设置" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "首次設定" + "value": "打開系統設定" } }, "ko": { "stringUnit": { "state": "translated", - "value": "최초 설정" + "value": "시스템 설정 열기" } }, "ar": { "stringUnit": { "state": "translated", - "value": "الإعداد الأولي" + "value": "فتح إعدادات النظام" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Prvo podešavanje" + "value": "Otvori Postavke sistema" } }, "da": { "stringUnit": { "state": "translated", - "value": "Første opsætning" + "value": "Åbn Systemindstillinger" } }, "it": { "stringUnit": { "state": "translated", - "value": "Prima configurazione" + "value": "Apri Impostazioni di Sistema" } }, "km": { "stringUnit": { "state": "translated", - "value": "ការរៀបចំលើកដំបូង" + "value": "បើកការកំណត់ប្រព័ន្ធ" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Førstegangsoppsett" + "value": "Åpne Systeminnstillinger" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Pierwsza konfiguracja" + "value": "Otwórz Ustawienia systemowe" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Primeira configuração" + "value": "Abrir Ajustes do Sistema" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Первая настройка" + "value": "Открыть системные настройки" } }, "th": { "stringUnit": { "state": "translated", - "value": "การตั้งค่าครั้งแรก" + "value": "เปิดการตั้งค่าระบบ" } }, "tr": { "stringUnit": { "state": "translated", - "value": "İlk kurulum" + "value": "Sistem Ayarlarını aç" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Перше налаштування" + "value": "Відкрити Системні параметри" } } } }, - "cloud.vpn.setup.steps.extension": { + "cli.browser.inputFiles.error.usage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "1. Click Connect Cloud VPN. When macOS asks, allow the cmux network extension in System Settings." + "value": "set-input-files requires --selector and either --file (repeatable) or --clear" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "1. Klicken Sie auf Cloud-VPN verbinden. Erlauben Sie auf Aufforderung von macOS die cmux-Netzwerkerweiterung in den Systemeinstellungen." + "value": "يتطلب set-input-files الخيار --selector مع --file (يمكن تكراره) أو --clear" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "1. Cliquez sur Connecter le VPN Cloud. Lorsque macOS le demande, autorisez l’extension réseau cmux dans Réglages Système." + "value": "set-input-files benötigt --selector und entweder --file (wiederholbar) oder --clear" } }, "es": { "stringUnit": { "state": "translated", - "value": "1. Haz clic en Conectar VPN de Cloud. Cuando macOS lo solicite, permite la extensión de red de cmux en Ajustes del Sistema." + "value": "set-input-files requiere --selector y --file (repetible) o --clear" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "set-input-files nécessite --selector et soit --file (répétable), soit --clear" } }, "ja": { "stringUnit": { "state": "translated", - "value": "1. 「Cloud VPNに接続」をクリックします。macOSに求められたら、システム設定でcmuxのネットワーク機能拡張を許可します。" + "value": "set-input-files には --selector と、--file (複数指定可)または --clear が必要です" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "set-input-files에는 --selector 와 --file (반복 가능) 또는 --clear가 필요합니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "1. 点击“连接 Cloud VPN”。macOS 提示时,在系统设置中允许 cmux 网络扩展。" + "value": "set-input-files 需要 --selector ,以及 --file (可重复)或 --clear" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "1. 按一下「連接 Cloud VPN」。macOS 提示時,請在系統設定中允許 cmux 網路延伸功能。" + "value": "set-input-files 需要 --selector ,以及 --file (可重複)或 --clear" } - }, - "ko": { + } + } + }, + "cli.browser.inputFiles.help": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "1. Cloud VPN 연결을 클릭합니다. macOS에서 요청하면 시스템 설정에서 cmux 네트워크 확장을 허용합니다." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Select local files (up to 128 files, 32 MiB total) or clear the selection. Directory uploads are unsupported." } }, "ar": { "stringUnit": { "state": "translated", - "value": "1. انقر على توصيل VPN الخاص بـ Cloud. عندما يطلب macOS ذلك، اسمح بامتداد شبكة cmux في إعدادات النظام." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n اختر ملفات محلية (حتى 128 ملفًا بإجمالي 32 MiB) أو امسح الاختيار. رفع المجلدات غير مدعوم." } }, - "bs": { + "de": { "stringUnit": { "state": "translated", - "value": "1. Kliknite Poveži Cloud VPN. Kada macOS zatraži, dozvolite cmux mrežni dodatak u Postavkama sistema." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Lokale Dateien auswählen (bis zu 128 Dateien, insgesamt 32 MiB) oder die Auswahl leeren. Ordner-Uploads werden nicht unterstützt." } }, - "da": { + "es": { "stringUnit": { "state": "translated", - "value": "1. Klik på Forbind Cloud VPN. Når macOS spørger, skal du tillade cmux-netværksudvidelsen i Systemindstillinger." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Selecciona archivos locales (hasta 128 archivos, 32 MiB en total) o borra la selección. No se admite subir carpetas." } }, - "it": { + "fr": { "stringUnit": { "state": "translated", - "value": "1. Fai clic su Connetti VPN Cloud. Quando macOS lo richiede, consenti l’estensione di rete cmux nelle Impostazioni di Sistema." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n Sélectionner des fichiers locaux (jusqu’à 128 fichiers, 32 MiB au total) ou effacer la sélection. L’envoi de dossiers n’est pas pris en charge." } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "1. ចុច ភ្ជាប់ Cloud VPN។ ពេល macOS ស្នើ សូមអនុញ្ញាតផ្នែកបន្ថែមបណ្តាញ cmux ក្នុងការកំណត់ប្រព័ន្ធ។" + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n ローカルファイルを選択(最大128個、合計32 MiB)するか、選択を解除します。ディレクトリのアップロードには対応していません。" } }, - "nb": { + "ko": { "stringUnit": { "state": "translated", - "value": "1. Klikk Koble til Cloud VPN. Når macOS spør, tillat cmux-nettverksutvidelsen i Systeminnstillinger." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n 로컬 파일을 선택하거나(최대 128개, 총 32 MiB) 선택을 지웁니다. 디렉터리 업로드는 지원되지 않습니다." } }, - "pl": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "1. Kliknij Połącz z Cloud VPN. Gdy macOS poprosi, zezwól na rozszerzenie sieci cmux w Ustawieniach systemowych." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n 选择本地文件(最多 128 个文件,总计 32 MiB)或清除选择。不支持上传目录。" } }, - "pt-BR": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "1. Clique em Conectar Cloud VPN. Quando o macOS pedir, permita a extensão de rede do cmux nos Ajustes do Sistema." + "value": "set-input-files --selector (--file ... | --clear) [--snapshot-after]\n 選取本機檔案(最多 128 個檔案,總計 32 MiB)或清除選取。不支援上傳目錄。" + } + } + } + }, + "browser.inputFiles.error.invalidTarget": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The selector must match a file input without directory selection" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "1. Нажмите Подключить Cloud VPN. Когда macOS попросит, разрешите сетевое расширение cmux в системных настройках." + "value": "يجب أن يطابق المحدد حقل إدخال ملفات لا يستخدم اختيار المجلدات" } }, - "th": { + "de": { "stringUnit": { "state": "translated", - "value": "1. คลิก เชื่อมต่อ Cloud VPN เมื่อ macOS ขอ ให้อนุญาตส่วนขยายเครือข่าย cmux ในการตั้งค่าระบบ" + "value": "Der Selektor muss ein Dateieingabefeld ohne Ordnerauswahl treffen" } }, - "tr": { + "es": { "stringUnit": { "state": "translated", - "value": "1. Cloud VPN’e bağlan’a tıklayın. macOS istediğinde Sistem Ayarları’nda cmux ağ uzantısına izin verin." + "value": "El selector debe coincidir con un campo de archivos sin selección de carpetas" } }, - "uk": { + "fr": { "stringUnit": { "state": "translated", - "value": "1. Натисніть Підключити Cloud VPN. Коли macOS попросить, дозвольте мережеве розширення cmux у Системних параметрах." + "value": "Le sélecteur doit correspondre à un champ de fichier sans sélection de dossier" } - } - } - }, - "cloud.vpn.setup.steps.settings": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ja": { "stringUnit": { "state": "translated", - "value": "Open General > Login Items & Extensions > Network Extensions, then enable cmux." + "value": "セレクターはディレクトリ選択を使わないファイル入力欄に一致する必要があります" } }, - "de": { + "ko": { "stringUnit": { "state": "translated", - "value": "Öffnen Sie Allgemein > Anmeldeobjekte & Erweiterungen > Netzwerkerweiterungen und aktivieren Sie cmux." + "value": "선택자는 디렉터리 선택이 없는 파일 입력 요소와 일치해야 합니다" } }, - "fr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Ouvrez Général > Ouverture et extensions > Extensions réseau, puis activez cmux." + "value": "选择器必须匹配未启用目录选择的文件输入字段" } }, - "es": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Abre General > Ítems de inicio y extensiones > Extensiones de red y activa cmux." + "value": "選取器必須符合未啟用目錄選取的檔案輸入欄位" + } + } + } + }, + "browser.inputFiles.error.multipleRequired": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This file input does not allow multiple files" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "「一般」>「ログイン項目と機能拡張」>「ネットワーク機能拡張」を開き、cmuxを有効にします。" + "value": "لا يسمح حقل إدخال الملفات هذا باختيار عدة ملفات" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "打开“通用”>“登录项与扩展”>“网络扩展”,然后启用 cmux。" + "value": "Dieses Dateieingabefeld erlaubt nicht mehrere Dateien" } }, - "zh-Hant": { + "es": { "stringUnit": { "state": "translated", - "value": "打開「一般」>「登入項目與延伸功能」>「網路延伸功能」,然後啟用 cmux。" + "value": "Este campo de archivos no permite seleccionar varios archivos" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "일반 > 로그인 항목 및 확장 프로그램 > 네트워크 확장을 열고 cmux를 활성화합니다." + "value": "Ce champ de fichier ne permet pas de sélectionner plusieurs fichiers" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "افتح عام > عناصر الدخول والامتدادات > امتدادات الشبكة، ثم فعّل cmux." + "value": "このファイル入力欄では複数のファイルを選択できません" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "Otvorite Opšte > Stavke za prijavu i dodaci > Mrežni dodaci, pa omogućite cmux." + "value": "이 파일 입력 요소는 여러 파일을 허용하지 않습니다" } }, - "da": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Åbn Generelt > Loginemner og udvidelser > Netværksudvidelser, og aktivér cmux." + "value": "此文件输入字段不允许选择多个文件" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Apri Generali > Elementi login ed estensioni > Estensioni di rete, quindi abilita cmux." + "value": "此檔案輸入欄位不允許選取多個檔案" + } + } + } + }, + "browser.inputFiles.error.cancelled": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File upload preparation was cancelled" } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "បើក ទូទៅ > ធាតុចូល និងផ្នែកបន្ថែម > ផ្នែកបន្ថែមបណ្តាញ ហើយបើក cmux។" + "value": "تم إلغاء تجهيز الملفات للرفع" } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Åpne Generelt > Påloggingselementer og utvidelser > Nettverksutvidelser, og aktiver cmux." + "value": "Die Vorbereitung des Datei-Uploads wurde abgebrochen" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Otwórz Ogólne > Elementy logowania i rozszerzenia > Rozszerzenia sieciowe, a następnie włącz cmux." + "value": "Se canceló la preparación de los archivos para subir" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Abra Geral > Itens de Início e Extensões > Extensões de Rede e ative o cmux." + "value": "La préparation des fichiers à envoyer a été annulée" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Откройте Основные > Объекты входа и расширения > Сетевые расширения и включите cmux." + "value": "ファイルのアップロード準備がキャンセルされました" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "เปิด ทั่วไป > รายการเข้าสู่ระบบและส่วนขยาย > ส่วนขยายเครือข่าย แล้วเปิดใช้ cmux" + "value": "파일 업로드 준비가 취소되었습니다" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Genel > Giriş Öğeleri ve Uzantılar > Ağ Uzantıları’nı açın ve cmux’u etkinleştirin." + "value": "已取消准备上传文件" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Відкрийте Загальні > Об’єкти входу та розширення > Мережеві розширення й увімкніть cmux." + "value": "已取消準備上傳檔案" } } } }, - "cloud.vpn.setup.steps.settingsLegacy": { + "browser.inputFiles.error.invalidParameters": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Open Privacy & Security and allow the cmux extension." + "value": "set-input-files requires a selector and a files array of absolute paths" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Öffnen Sie Datenschutz & Sicherheit und erlauben Sie die cmux-Erweiterung." + "value": "يتطلب set-input-files محددًا ومصفوفة files تحتوي على مسارات مطلقة" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "Ouvrez Confidentialité et sécurité et autorisez l’extension cmux." + "value": "set-input-files benötigt einen Selektor und ein files-Array mit absoluten Pfaden" } }, "es": { "stringUnit": { "state": "translated", - "value": "Abre Privacidad y seguridad y permite la extensión de cmux." + "value": "set-input-files requiere un selector y una matriz files de rutas absolutas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "set-input-files nécessite un sélecteur et un tableau files de chemins absolus" } }, "ja": { "stringUnit": { "state": "translated", - "value": "「プライバシーとセキュリティ」を開き、cmuxの機能拡張を許可します。" + "value": "set-input-files にはセレクターと絶対パスを含む files 配列が必要です" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "set-input-files에는 선택자와 절대 경로로 구성된 files 배열이 필요합니다" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "打开“隐私与安全性”,然后允许 cmux 扩展。" + "value": "set-input-files 需要选择器及包含绝对路径的 files 数组" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "打開「隱私權與安全性」,然後允許 cmux 延伸功能。" + "value": "set-input-files 需要選取器及包含絕對路徑的 files 陣列" } - }, - "ko": { + } + } + }, + "browser.inputFiles.error.invalidSelection": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "개인정보 보호 및 보안을 열고 cmux 확장을 허용합니다." + "value": "Select at most 128 files using absolute paths" } }, "ar": { "stringUnit": { "state": "translated", - "value": "افتح الخصوصية والأمان واسمح بامتداد cmux." + "value": "اختر ما يصل إلى 128 ملفًا باستخدام مسارات مطلقة" } }, - "bs": { + "de": { "stringUnit": { "state": "translated", - "value": "Otvorite Privatnost i sigurnost i dozvolite cmux dodatak." + "value": "Wählen Sie höchstens 128 Dateien mit absoluten Pfaden aus" } }, - "da": { + "es": { "stringUnit": { "state": "translated", - "value": "Åbn Anonymitet og sikkerhed, og tillad cmux-udvidelsen." + "value": "Selecciona como máximo 128 archivos mediante rutas absolutas" } }, - "it": { + "fr": { "stringUnit": { "state": "translated", - "value": "Apri Privacy e sicurezza e consenti l’estensione cmux." + "value": "Sélectionnez au maximum 128 fichiers avec des chemins absolus" } }, - "km": { + "ja": { "stringUnit": { "state": "translated", - "value": "បើក ឯកជនភាព និងសុវត្ថិភាព ហើយអនុញ្ញាតផ្នែកបន្ថែម cmux។" + "value": "絶対パスで最大128個のファイルを選択してください" } }, - "nb": { + "ko": { "stringUnit": { "state": "translated", - "value": "Åpne Personvern og sikkerhet og tillat cmux-utvidelsen." + "value": "절대 경로를 사용하여 최대 128개의 파일을 선택하세요" } }, - "pl": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Otwórz Prywatność i ochrona i zezwól na rozszerzenie cmux." + "value": "请使用绝对路径选择最多 128 个文件" } }, - "pt-BR": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Abra Privacidade e Segurança e permita a extensão do cmux." + "value": "請使用絕對路徑選取最多 128 個檔案" + } + } + } + }, + "browser.inputFiles.error.timeout": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Timed out preparing files for upload" } }, - "ru": { + "ar": { "stringUnit": { "state": "translated", - "value": "Откройте Конфиденциальность и безопасность и разрешите расширение cmux." + "value": "انتهت مهلة تجهيز الملفات للرفع" } }, - "th": { + "de": { "stringUnit": { "state": "translated", - "value": "เปิด ความเป็นส่วนตัวและความปลอดภัย แล้วอนุญาตส่วนขยาย cmux" + "value": "Zeitüberschreitung beim Vorbereiten der Dateien für den Upload" } }, - "tr": { + "es": { "stringUnit": { "state": "translated", - "value": "Gizlilik ve Güvenlik’i açın ve cmux uzantısına izin verin." + "value": "Se agotó el tiempo de preparación de los archivos para subir" } }, - "uk": { + "fr": { "stringUnit": { "state": "translated", - "value": "Відкрийте Конфіденційність і безпека та дозвольте розширення cmux." + "value": "Le délai de préparation des fichiers à envoyer a été dépassé" } - } - } - }, - "cloud.vpn.setup.steps.configuration": { - "extractionState": "manual", - "localizations": { - "en": { + }, + "ja": { "stringUnit": { "state": "translated", - "value": "2. Allow cmux to add a VPN configuration named cmux Cloud. This is a separate macOS permission." + "value": "アップロードするファイルの準備がタイムアウトしました" } }, - "de": { + "ko": { "stringUnit": { "state": "translated", - "value": "2. Erlauben Sie cmux, eine VPN-Konfiguration namens cmux Cloud hinzuzufügen. Dies ist eine separate macOS-Berechtigung." + "value": "업로드할 파일 준비 시간이 초과되었습니다" } }, - "fr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "2. Autorisez cmux à ajouter une configuration VPN nommée cmux Cloud. Il s’agit d’une autorisation macOS distincte." + "value": "准备上传文件超时" } }, - "es": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "2. Permite que cmux añada una configuración VPN llamada cmux Cloud. Este es un permiso independiente de macOS." + "value": "準備上傳檔案逾時" + } + } + } + }, + "browser.inputFiles.error.tooLarge": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The combined upload must be no larger than 32 MiB" } }, - "ja": { + "ar": { "stringUnit": { "state": "translated", - "value": "2. cmux Cloudという名前のVPN構成の追加を許可します。これは別のmacOS権限です。" + "value": "يجب ألا يتجاوز الحجم الإجمالي للملفات المرفوعة 32 MiB" } }, - "zh-Hans": { + "de": { "stringUnit": { "state": "translated", - "value": "2. 允许 cmux 添加名为 cmux Cloud 的 VPN 配置。这是单独的 macOS 权限。" + "value": "Die Uploads dürfen insgesamt nicht größer als 32 MiB sein" } }, - "zh-Hant": { + "es": { "stringUnit": { "state": "translated", - "value": "2. 允許 cmux 加入名為 cmux Cloud 的 VPN 設定。這是另一項 macOS 權限。" + "value": "El tamaño total de los archivos para subir no debe superar los 32 MiB" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "2. cmux가 cmux Cloud라는 VPN 구성을 추가하도록 허용합니다. 이는 별도의 macOS 권한입니다." + "value": "La taille totale des fichiers à envoyer ne doit pas dépasser 32 MiB" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "2. اسمح لـ cmux بإضافة إعداد VPN باسم cmux Cloud. هذا إذن منفصل في macOS." + "value": "アップロードするファイルの合計サイズは32 MiB以下にしてください" } }, - "bs": { + "ko": { "stringUnit": { "state": "translated", - "value": "2. Dozvolite cmux-u da doda VPN konfiguraciju pod imenom cmux Cloud. Ovo je posebna macOS dozvola." + "value": "전체 업로드 크기는 32 MiB를 초과할 수 없습니다" } }, - "da": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "2. Tillad cmux at tilføje en VPN-konfiguration med navnet cmux Cloud. Dette er en separat macOS-tilladelse." + "value": "上传文件的总大小不得超过 32 MiB" } }, - "it": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "2. Consenti a cmux di aggiungere una configurazione VPN chiamata cmux Cloud. È un’autorizzazione macOS separata." + "value": "上傳檔案的總大小不得超過 32 MiB" + } + } + } + }, + "browser.inputFiles.error.unreadableFile": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Every upload path must be a readable regular file" } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "2. អនុញ្ញាតឱ្យ cmux បន្ថែមការកំណត់ VPN ឈ្មោះ cmux Cloud។ នេះជាសិទ្ធិ macOS ផ្សេងមួយ។" + "value": "يجب أن يشير كل مسار للرفع إلى ملف عادي قابل للقراءة" } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "2. Tillat cmux å legge til en VPN-konfigurasjon kalt cmux Cloud. Dette er en separat macOS-tillatelse." + "value": "Jeder Upload-Pfad muss auf eine lesbare reguläre Datei verweisen" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "2. Zezwól cmux dodać konfigurację VPN o nazwie cmux Cloud. To osobne uprawnienie macOS." + "value": "Cada ruta de subida debe apuntar a un archivo normal que se pueda leer" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "2. Permita que o cmux adicione uma configuração VPN chamada cmux Cloud. Esta é uma permissão separada do macOS." + "value": "Chaque chemin à envoyer doit désigner un fichier ordinaire lisible" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "2. Разрешите cmux добавить VPN-конфигурацию с именем cmux Cloud. Это отдельное разрешение macOS." + "value": "各アップロードパスは読み取り可能な通常ファイルである必要があります" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "2. อนุญาตให้ cmux เพิ่มการกำหนดค่า VPN ชื่อ cmux Cloud นี่เป็นสิทธิ์ macOS แยกต่างหาก" + "value": "모든 업로드 경로는 읽을 수 있는 일반 파일이어야 합니다" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "2. cmux’un cmux Cloud adlı bir VPN yapılandırması eklemesine izin verin. Bu ayrı bir macOS iznidir." + "value": "每个上传路径都必须指向可读取的普通文件" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "2. Дозвольте cmux додати конфігурацію VPN з назвою cmux Cloud. Це окремий дозвіл macOS." + "value": "每個上傳路徑都必須指向可讀取的一般檔案" } } } }, - "cloud.vpn.setup.steps.return": { + "cloudTree.group.cloudMachines.usage": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "3. Return to this pane. The connection continues automatically after approval. Disconnect here when finished. Quitting cmux or signing out also disconnects it." + "value": "%1$d/%2$d" } }, "de": { "stringUnit": { "state": "translated", - "value": "3. Kehren Sie zu diesem Bereich zurück. Die Verbindung wird nach der Genehmigung automatisch fortgesetzt. Trennen Sie sie hier, wenn Sie fertig sind. Das Beenden von cmux oder Abmelden trennt sie ebenfalls." + "value": "%1$d/%2$d" } }, "fr": { "stringUnit": { "state": "translated", - "value": "3. Revenez à ce volet. La connexion reprend automatiquement après autorisation. Déconnectez-la ici lorsque vous avez terminé. Quitter cmux ou fermer la session la déconnecte aussi." + "value": "%1$d/%2$d" } }, - "es": { + "ar": { "stringUnit": { "state": "translated", - "value": "3. Vuelve a este panel. La conexión continúa automáticamente tras la autorización. Desconéctala aquí cuando termines. Salir de cmux o cerrar sesión también la desconecta." + "value": "%1$d/%2$d" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "3. このペインに戻ります。承認後、接続は自動的に続行されます。使い終わったらここで切断してください。cmuxの終了やサインアウトでも切断されます。" + "value": "%1$d/%2$d" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "3. 返回此面板。批准后连接会自动继续。完成后可在此断开连接。退出 cmux 或退出登录也会断开连接。" + "value": "%1$d/%2$d" } }, - "zh-Hant": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "3. 返回此面板。核准後連線會自動繼續。完成後可在此中斷連線。結束 cmux 或登出也會中斷連線。" + "value": "%1$d/%2$d" } }, "ko": { "stringUnit": { "state": "translated", - "value": "3. 이 패널로 돌아옵니다. 승인 후 연결이 자동으로 계속됩니다. 사용을 마치면 여기에서 연결을 해제하세요. cmux를 종료하거나 로그아웃해도 연결이 해제됩니다." + "value": "%1$d/%2$d" } }, - "ar": { + "ja": { "stringUnit": { "state": "translated", - "value": "3. ارجع إلى هذه اللوحة. يستمر الاتصال تلقائيًا بعد الموافقة. اقطع الاتصال هنا عند الانتهاء. يؤدي إنهاء cmux أو تسجيل الخروج أيضًا إلى قطع الاتصال." + "value": "%1$d/%2$d" } }, "bs": { "stringUnit": { "state": "translated", - "value": "3. Vratite se na ovaj panel. Veza se nastavlja automatski nakon odobrenja. Ovdje prekinite vezu kada završite. Izlazak iz cmux-a ili odjava takođe prekida vezu." + "value": "%1$d/%2$d" } }, "da": { "stringUnit": { "state": "translated", - "value": "3. Gå tilbage til dette panel. Forbindelsen fortsætter automatisk efter godkendelsen. Afbryd her, når du er færdig. Hvis du afslutter cmux eller logger ud, afbrydes forbindelsen også." + "value": "%1$d/%2$d" } }, "it": { "stringUnit": { "state": "translated", - "value": "3. Torna a questo pannello. La connessione continua automaticamente dopo l’autorizzazione. Disconnettila qui quando hai finito. Anche uscire da cmux o effettuare il logout la disconnette." + "value": "%1$d/%2$d" } }, "km": { "stringUnit": { "state": "translated", - "value": "3. ត្រឡប់ទៅផ្ទាំងនេះ។ ការភ្ជាប់នឹងបន្តដោយស្វ័យប្រវត្តិបន្ទាប់ពីអនុម័ត។ ផ្តាច់នៅទីនេះពេលបញ្ចប់។ ការបិទ cmux ឬចាកចេញក៏ផ្តាច់ការភ្ជាប់ដែរ។" + "value": "%1$d/%2$d" } }, "nb": { "stringUnit": { "state": "translated", - "value": "3. Gå tilbake til dette panelet. Forbindelsen fortsetter automatisk etter godkjenning. Koble fra her når du er ferdig. Avslutning av cmux eller utlogging kobler også fra." + "value": "%1$d/%2$d" } }, "pl": { "stringUnit": { "state": "translated", - "value": "3. Wróć do tego panelu. Połączenie będzie kontynuowane automatycznie po zatwierdzeniu. Po zakończeniu rozłącz je tutaj. Zamknięcie cmux lub wylogowanie również rozłączy połączenie." + "value": "%1$d/%2$d" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "3. Volte a este painel. A conexão continuará automaticamente após a aprovação. Desconecte aqui quando terminar. Sair do cmux ou encerrar a sessão também desconecta." + "value": "%1$d/%2$d" } }, "ru": { "stringUnit": { "state": "translated", - "value": "3. Вернитесь в эту панель. После подтверждения соединение продолжится автоматически. Завершив работу, отключите его здесь. Выход из cmux или системы также отключает соединение." + "value": "%1$d/%2$d" } }, "th": { "stringUnit": { "state": "translated", - "value": "3. กลับมาที่แผงนี้ การเชื่อมต่อจะดำเนินต่อโดยอัตโนมัติหลังการอนุมัติ ตัดการเชื่อมต่อที่นี่เมื่อเสร็จแล้ว การออกจาก cmux หรือออกจากระบบจะตัดการเชื่อมต่อด้วย" + "value": "%1$d/%2$d" } }, "tr": { "stringUnit": { "state": "translated", - "value": "3. Bu panele dönün. Onaydan sonra bağlantı otomatik olarak devam eder. Bitirdiğinizde buradan bağlantıyı kesin. cmux’tan çıkmak veya oturumu kapatmak da bağlantıyı keser." + "value": "%1$d/%2$d" } }, "uk": { "stringUnit": { "state": "translated", - "value": "3. Поверніться до цієї панелі. Після схвалення з’єднання продовжиться автоматично. Завершивши роботу, від’єднайте його тут. Вихід із cmux або системи також від’єднає з’єднання." + "value": "%1$d/%2$d" } } } }, - "cloud.vpn.setup.address.title": { - "extractionState": "manual", + "settings.app.accentColor.custom": { "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Private addresses" - } - }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Private Adressen" + "value": "مخصص" } }, - "fr": { + "bs": { "stringUnit": { "state": "translated", - "value": "Adresses privées" + "value": "Prilagođeno" } }, - "es": { + "da": { "stringUnit": { "state": "translated", - "value": "Direcciones privadas" + "value": "Tilpasset" } }, - "ja": { + "de": { "stringUnit": { "state": "translated", - "value": "プライベートアドレス" + "value": "Benutzerdefiniert" } }, - "zh-Hans": { + "en": { "stringUnit": { "state": "translated", - "value": "私有地址" + "value": "Custom" } }, - "zh-Hant": { + "es": { "stringUnit": { "state": "translated", - "value": "私有位址" + "value": "Personalizado" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "비공개 주소" + "value": "Personnalisée" } }, - "ar": { + "it": { "stringUnit": { "state": "translated", - "value": "العناوين الخاصة" + "value": "Personalizzato" } }, - "bs": { + "ja": { "stringUnit": { "state": "translated", - "value": "Privatne adrese" + "value": "カスタム" } }, - "da": { + "ko": { "stringUnit": { "state": "translated", - "value": "Private adresser" + "value": "사용자 지정" } }, - "it": { + "nb": { "stringUnit": { "state": "translated", - "value": "Indirizzi privati" + "value": "Egendefinert" } }, - "km": { + "pl": { "stringUnit": { "state": "translated", - "value": "អាសយដ្ឋានឯកជន" + "value": "Niestandardowy" } }, - "nb": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Private adresser" + "value": "Personalizado" } }, - "pl": { + "ru": { "stringUnit": { "state": "translated", - "value": "Prywatne adresy" + "value": "Пользовательский" } }, - "pt-BR": { + "th": { "stringUnit": { "state": "translated", - "value": "Endereços privados" + "value": "กำหนดเอง" } }, - "ru": { + "tr": { "stringUnit": { "state": "translated", - "value": "Частные адреса" + "value": "Özel" } }, - "th": { + "uk": { "stringUnit": { "state": "translated", - "value": "ที่อยู่ส่วนตัว" + "value": "Власний" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Özel adresler" + "value": "自定义" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Приватні адреси" + "value": "自訂" } } } }, - "cloud.vpn.setup.openSettings": { + "command.rightSidebarToggleButton.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Open System Settings" + "value": "View" } }, - "de": { + "ar": { "stringUnit": { "state": "translated", - "value": "Systemeinstellungen öffnen" + "value": "العرض" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "Ouvrir Réglages Système" + "value": "Ansicht" } }, "es": { "stringUnit": { "state": "translated", - "value": "Abrir Ajustes del Sistema" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "システム設定を開く" + "value": "Vista" } }, - "zh-Hans": { + "fr": { "stringUnit": { "state": "translated", - "value": "打开系统设置" + "value": "Présentation" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "打開系統設定" + "value": "表示" } }, "ko": { "stringUnit": { "state": "translated", - "value": "시스템 설정 열기" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "فتح إعدادات النظام" + "value": "보기" } }, - "bs": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Otvori Postavke sistema" + "value": "视图" } }, - "da": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Åbn Systemindstillinger" + "value": "顯示方式" } - }, - "it": { + } + } + }, + "command.rightSidebarToggleButton.title": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "Apri Impostazioni di Sistema" + "value": "Right Sidebar Button: %@" } }, - "km": { + "ar": { "stringUnit": { "state": "translated", - "value": "បើកការកំណត់ប្រព័ន្ធ" + "value": "زر الشريط الجانبي الأيمن: %@" } }, - "nb": { + "de": { "stringUnit": { "state": "translated", - "value": "Åpne Systeminnstillinger" + "value": "Schaltfläche der rechten Seitenleiste: %@" } }, - "pl": { + "es": { "stringUnit": { "state": "translated", - "value": "Otwórz Ustawienia systemowe" + "value": "Botón de la barra lateral derecha: %@" } }, - "pt-BR": { + "fr": { "stringUnit": { "state": "translated", - "value": "Abrir Ajustes do Sistema" + "value": "Bouton de la barre latérale droite : %@" } }, - "ru": { + "ja": { "stringUnit": { "state": "translated", - "value": "Открыть системные настройки" + "value": "右サイドバーボタン: %@" } }, - "th": { + "ko": { "stringUnit": { "state": "translated", - "value": "เปิดการตั้งค่าระบบ" + "value": "오른쪽 사이드바 버튼: %@" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Sistem Ayarlarını aç" + "value": "右侧边栏按钮:%@" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Відкрити Системні параметри" + "value": "右側邊欄按鈕:%@" } } } }, - "settings.app.accentColor.custom": { + "rightSidebar.toggleButton.placement.hidden": { + "extractionState": "manual", "localizations": { - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "مخصص" + "value": "Hidden" } }, - "bs": { + "ar": { "stringUnit": { "state": "translated", - "value": "Prilagođeno" + "value": "مخفي" } }, - "da": { + "de": { "stringUnit": { "state": "translated", - "value": "Tilpasset" + "value": "Ausgeblendet" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "Benutzerdefiniert" + "value": "Oculto" } }, - "en": { + "fr": { "stringUnit": { "state": "translated", - "value": "Custom" + "value": "Masqué" } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "Personalizado" + "value": "非表示" } }, - "fr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Personnalisée" + "value": "숨김" } }, - "it": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Personalizzato" + "value": "隐藏" } }, - "ja": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "カスタム" + "value": "隱藏" } - }, - "ko": { + } + } + }, + "rightSidebar.toggleButton.placement.paneTabBar": { + "extractionState": "manual", + "localizations": { + "en": { "stringUnit": { "state": "translated", - "value": "사용자 지정" + "value": "Pane Tab Bar" } }, - "nb": { + "ar": { "stringUnit": { "state": "translated", - "value": "Egendefinert" + "value": "شريط علامات تبويب الجزء" } }, - "pl": { + "de": { "stringUnit": { "state": "translated", - "value": "Niestandardowy" + "value": "Tab-Leiste des Bereichs" } }, - "pt-BR": { + "es": { "stringUnit": { "state": "translated", - "value": "Personalizado" + "value": "Barra de pestañas del panel" } }, - "ru": { + "fr": { "stringUnit": { "state": "translated", - "value": "Пользовательский" + "value": "Barre d’onglets du panneau" } }, - "th": { + "ja": { "stringUnit": { "state": "translated", - "value": "กำหนดเอง" + "value": "ペインのタブバー" } }, - "tr": { + "ko": { "stringUnit": { "state": "translated", - "value": "Özel" + "value": "패널 탭 막대" } }, - "uk": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "Власний" + "value": "窗格标签栏" } }, - "zh-Hans": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "自定义" + "value": "窗格標籤列" + } + } + } + }, + "rightSidebar.toggleButton.placement.sidebarFooter": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Left Sidebar Footer" } }, - "zh-Hant": { + "ar": { "stringUnit": { "state": "translated", - "value": "自訂" + "value": "تذييل الشريط الجانبي الأيسر" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fußzeile der linken Seitenleiste" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pie de la barra lateral izquierda" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pied de la barre latérale gauche" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "左サイドバーのフッター" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "왼쪽 사이드바 하단" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "左侧边栏底部" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "左側邊欄底部" + } + } + } + }, + "rightSidebar.toggleButton.placement.titlebar": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Window Corner" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "زاوية النافذة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fensterecke" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esquina de la ventana" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coin de la fenêtre" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ウインドウの角" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "창 모서리" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "窗口角落" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "視窗角落" + } + } + } + }, + "rightSidebar.toggleButton.accessibilityLabel": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggle Right Sidebar" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تبديل الشريط الجانبي الأيمن" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rechte Seitenleiste ein-/ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Alternar barra lateral derecha" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher/masquer la barre latérale droite" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "右サイドバーを切り替え" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오른쪽 사이드바 전환" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "切换右侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "切換右側邊欄" + } + } + } + }, + "rightSidebar.toggleButton.accessibilityValue.hidden": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hidden" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مخفي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgeblendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oculta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquée" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "非表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "숨겨짐" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "已隐藏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已隱藏" + } + } + } + }, + "rightSidebar.toggleButton.accessibilityValue.shown": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shown" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ظاهر" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eingeblendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Visible" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affichée" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "表示中" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "표시됨" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "已显示" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已顯示" + } + } + } + }, + "rightSidebar.toggleButton.hide.tooltip": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide Right Sidebar" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إخفاء الشريط الجانبي الأيمن" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rechte Seitenleiste ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ocultar barra lateral derecha" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer la barre latérale droite" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "右サイドバーを隠す" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오른쪽 사이드바 숨기기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "隐藏右侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "隱藏右側邊欄" + } + } + } + }, + "rightSidebar.toggleButton.show.tooltip": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Right Sidebar" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار الشريط الجانبي الأيمن" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rechte Seitenleiste einblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar barra lateral derecha" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher la barre latérale droite" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "右サイドバーを表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오른쪽 사이드바 표시" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示右侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示右側邊欄" + } + } + } + }, + "debug.menu.rightSidebarToggleButton": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Right Sidebar Button" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "زر الشريط الجانبي الأيمن" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schaltfläche der rechten Seitenleiste" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Botón de la barra lateral derecha" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bouton de la barre latérale droite" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "右サイドバーボタン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오른쪽 사이드바 버튼" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "右侧边栏按钮" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "右側邊欄按鈕" + } + } + } + }, + "terminal.clipboardReadConfirmation.deny": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deny" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "رفض" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablehnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Denegar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Refuser" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "拒否" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "거부" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "拒绝" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "拒絕" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Odbij" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Afvis" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nega" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បដិសេធ" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Avslå" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Odmów" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Negar" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Запретить" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ปฏิเสธ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Reddet" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Заборонити" + } + } + } + }, + "terminal.clipboardReadConfirmation.message": { + "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." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يريد برنامج في هذه الطرفية قراءة الحافظة. اسمح بذلك فقط إذا كنت تثق بالبرنامج." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Programm in diesem Terminal möchte deine Zwischenablage lesen. Erlaube es nur, wenn du dem Programm vertraust." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Un programa de esta terminal quiere leer tu portapapeles. Permítelo solo si confías en el programa." + } + }, + "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." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このターミナルのプログラムがクリップボードを読み取ろうとしています。信頼できるプログラムの場合にのみ許可してください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 터미널의 프로그램이 클립보드를 읽으려고 합니다. 프로그램을 신뢰할 수 있을 때만 허용하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此终端中的程序想要读取你的剪贴板。只有在你信任该程序时才允许。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此終端機中的程式想要讀取你的剪貼簿。只有在你信任該程式時才允許。" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Program u ovom terminalu želi čitati vaš međuspremnik. Dozvolite samo ako vjerujete programu." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "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": "Un programma in questo terminale vuole leggere gli appunti. Consentilo solo se ti fidi del programma." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីក្នុងស្ថានីយនេះចង់អានក្ដារតម្បៀតខ្ទាស់របស់អ្នក។ អនុញ្ញាតតែបើអ្នកទុកចិត្តកម្មវិធីនេះ។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Et program i denne terminalen vil lese utklippstavlen din. Tillat det bare hvis du stoler på programmet." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Program w tym terminalu chce odczytać schowek. Zezwól tylko, jeśli ufasz temu programowi." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um programa neste terminal quer ler sua área de transferência. Permita somente se você confiar no programa." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Программа в этом терминале хочет прочитать буфер обмена. Разрешайте только программам, которым доверяете." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โปรแกรมในเทอร์มินัลนี้ต้องการอ่านคลิปบอร์ดของคุณ อนุญาตเฉพาะเมื่อคุณเชื่อถือโปรแกรมนี้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu terminaldeki bir program panonuzu okumak istiyor. Yalnızca programa güveniyorsanız izin verin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Програма в цьому терміналі хоче прочитати ваш буфер обміну. Дозволяйте, лише якщо довіряєте програмі." + } + } + } + }, + "terminal.clipboardReadConfirmation.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Allow Clipboard Access?" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "السماح بالوصول إلى الحافظة؟" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zugriff auf die Zwischenablage erlauben?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Permitir el acceso al portapapeles?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser l’accès au presse-papiers ?" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クリップボードへのアクセスを許可しますか?" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클립보드 접근을 허용하시겠습니까?" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "允许访问剪贴板?" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "允許存取剪貼簿?" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Dozvoliti pristup međuspremniku?" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Tillad adgang til udklipsholderen?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Consentire l'accesso agli appunti?" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "អនុញ្ញាតឱ្យចូលប្រើក្ដារតម្បៀតខ្ទាស់?" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Tillat tilgang til utklippstavlen?" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Zezwolić na dostęp do schowka?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permitir acesso à área de transferência?" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Разрешить доступ к буферу обмена?" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "อนุญาตให้เข้าถึงคลิปบอร์ดหรือไม่?" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Panoya erişime izin verilsin mi?" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Дозволити доступ до буфера обміну?" + } + } + } + }, + "notification.remoteRelay.hostFallback": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remote host" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مضيف بعيد" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Remote-Host" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Host remoto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Hôte distant" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "リモートホスト" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "원격 호스트" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "远程主机" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "遠端主機" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Udaljeni host" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Fjernvært" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Host remoto" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ម៉ាស៊ីនពីចម្ងាយ" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Ekstern vert" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Zdalny host" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Host remoto" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Удалённый хост" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โฮสต์ระยะไกล" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Uzak ana bilgisayar" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Віддалений хост" + } + } + } + }, + "notification.remoteRelay.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "%1$@: %2$@" + } + } + } + }, + "shortcut.fileExplorerQuickLook.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File Explorer: Quick Look" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مستكشف الملفات: نظرة سريعة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei-Explorer: Übersicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorador de archivos: Vista rápida" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Explorateur de fichiers : Coup d’œil" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ファイルエクスプローラ: クイックルック" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "파일 탐색기: 훑어보기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "文件资源管理器:快速查看" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "檔案瀏覽器:快速查看" + } + } + } + }, + "shortcut.fileExplorerRenameSelection.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File Explorer: Rename" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مستكشف الملفات: إعادة تسمية" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei-Explorer: Umbenennen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorador de archivos: Cambiar nombre" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Explorateur de fichiers : renommer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ファイルエクスプローラ: 名前を変更" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "파일 탐색기: 이름 변경" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "文件资源管理器:重命名" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "檔案瀏覽器:重新命名" + } + } + } + }, + "shortcut.fileExplorerSelectParent.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File Explorer: Enclosing Folder" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مستكشف الملفات: المجلد الأصل" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei-Explorer: Übergeordneter Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorador de archivos: Carpeta contenedora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Explorateur de fichiers : dossier parent" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ファイルエクスプローラ: 親フォルダ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "파일 탐색기: 상위 폴더" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "文件资源管理器:上层文件夹" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "檔案瀏覽器:上層檔案夾" + } + } + } + }, + "shortcut.fileExplorerToggleHiddenFiles.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File Explorer: Show Hidden Files" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مستكشف الملفات: إظهار الملفات المخفية" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei-Explorer: Versteckte Dateien anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorador de archivos: Mostrar archivos ocultos" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Explorateur de fichiers : afficher les fichiers masqués" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ファイルエクスプローラ: 隠しファイルを表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "파일 탐색기: 숨김 파일 보기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "文件资源管理器:显示隐藏文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "檔案瀏覽器:顯示隱藏檔案" + } + } + } + }, + "fileExplorer.omittedEntries": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This folder has more items than the remote listing returns." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يحتوي هذا المجلد على عناصر أكثر مما تُرجعه القائمة البعيدة." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Ordner enthält mehr Objekte, als die Remote-Auflistung liefert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta carpeta tiene más elementos de los que devuelve el listado remoto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce dossier contient plus d’éléments que la liste distante n’en renvoie." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このフォルダには、リモートの一覧で返されるより多くの項目があります。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 폴더에는 원격 목록이 반환하는 것보다 많은 항목이 있습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此文件夹中的项目多于远程列表返回的数量。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此檔案夾的項目多於遠端列表所傳回的數量。" + } + } + } + }, + "fileExplorer.header.viewOptions": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View Options" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "خيارات العرض" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Darstellungsoptionen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opciones de visualización" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Options de présentation" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "表示オプション" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "보기 옵션" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示选项" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示方式選項" + } + } + } + }, + "fileExplorer.contextMenu.moveToTrash": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move to Trash" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نقل إلى سلة المهملات" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In den Papierkorb legen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Trasladar a la papelera" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Placer dans la corbeille" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ゴミ箱に入れる" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "휴지통으로 이동" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "移到废纸篓" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "丟到垃圾桶" + } + } + } + }, + "fileExplorer.contextMenu.newFile": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New File" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ملف جديد" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Datei" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nuevo archivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau fichier" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規ファイル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 파일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增檔案" + } + } + } + }, + "fileExplorer.contextMenu.newFolder": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New Folder" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مجلد جديد" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuer Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nueva carpeta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau dossier" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規フォルダ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 폴더" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建文件夹" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增檔案夾" + } + } + } + }, + "fileExplorer.contextMenu.open": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "فتح" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "開く" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "열기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "打开" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "打開" + } + } + } + }, + "fileExplorer.contextMenu.quickLook": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Quick Look" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نظرة سريعة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Übersicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vista rápida" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Coup d’œil" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クイックルック" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "훑어보기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "快速查看" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "快速查看" + } + } + } + }, + "fileExplorer.contextMenu.refresh": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Refresh" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تحديث" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktualisieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再読み込み" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새로 고침" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "刷新" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新整理" + } + } + } + }, + "fileExplorer.contextMenu.rename": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rename" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة تسمية" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Umbenennen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar nombre" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renommer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "名前を変更" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이름 변경" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重命名" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新命名" + } + } + } + }, + "fileExplorer.contextMenu.showHiddenFiles": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Hidden Files" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار الملفات المخفية" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Versteckte Dateien anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar archivos ocultos" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher les fichiers masqués" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "隠しファイルを表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "숨김 파일 보기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示隐藏文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示隱藏檔案" + } + } + } + }, + "fileExplorer.contextMenu.sortBy": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sort By" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ترتيب حسب" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sortieren nach" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ordenar por" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trier par" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "並べ替え" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "정렬 기준" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "排序方式" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "排序方式" + } + } + } + }, + "fileExplorer.operation.failedTitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The operation couldn’t be completed" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذر إكمال العملية" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Vorgang konnte nicht abgeschlossen werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido completar la operación" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’opération n’a pas pu être effectuée" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "操作を完了できませんでした" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "작업을 완료할 수 없습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法完成操作" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法完成操作" + } + } + } + }, + "fileExplorer.sort.ascending": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ascending" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تصاعدي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aufsteigend" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ascendente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Croissant" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "昇順" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오름차순" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "升序" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "遞增" + } + } + } + }, + "fileExplorer.sort.dateModified": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Date Modified" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تاريخ التعديل" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Änderungsdatum" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fecha de modificación" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Date de modification" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "変更日" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "수정일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "修改日期" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "修改日期" + } + } + } + }, + "fileExplorer.sort.descending": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Descending" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تنازلي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Absteigend" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descendente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Décroissant" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "降順" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "내림차순" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "降序" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "遞減" + } + } + } + }, + "fileExplorer.sort.foldersFirst": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep Folders on Top" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إبقاء المجلدات في الأعلى" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner oben behalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mantener las carpetas arriba" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Garder les dossiers en haut" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "フォルダを上部に表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "폴더를 상단에 유지" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "将文件夹保持在顶部" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "將檔案夾保持在頂部" + } + } + } + }, + "fileExplorer.sort.kind": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Kind" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "النوع" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Art" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clase" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Type" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "種類" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "종류" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "种类" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "種類" + } + } + } + }, + "fileExplorer.sort.name": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Name" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الاسم" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Name" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nombre" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "名前" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이름" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "名称" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "名稱" + } + } + } + }, + "fileExplorer.sort.size": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Size" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الحجم" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Größe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tamaño" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Taille" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイズ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "크기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "大小" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "大小" + } + } + } + }, + "fileExplorer.newFile.defaultName": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "untitled file" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ملف بلا عنوان" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Unbenannte Datei" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "archivo sin título" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fichier sans titre" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "名称未設定ファイル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "무제 파일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "未命名文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "未命名檔案" + } + } + } + }, + "fileExplorer.newFolder.defaultName": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "untitled folder" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مجلد بلا عنوان" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuer Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "carpeta sin título" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "dossier sans titre" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "名称未設定フォルダ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "무제 폴더" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "未命名文件夹" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "未命名檔案夾" + } + } + } + }, + "fileExplorer.operation.alreadyExists": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The name “%@” is already taken. Choose a different name." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الاسم ”%@“ مستخدم بالفعل. اختر اسمًا مختلفًا." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Name „%@“ ist bereits vergeben. Wähle einen anderen Namen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El nombre “%@” ya está en uso. Elige otro nombre." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le nom « %@ » est déjà utilisé. Choisissez un autre nom." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "名前“%@”はすでに使用されています。別の名前を選択してください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "“%@” 이름은 이미 사용 중입니다. 다른 이름을 선택하십시오." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "名称“%@”已被使用。请选择其他名称。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "名稱「%@」已被使用。請選擇其他名稱。" + } + } + } + }, + "fileExplorer.operation.invalidName": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Names cannot be empty or contain “/” or “:”." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لا يمكن أن تكون الأسماء فارغة أو تحتوي على ”/“ أو ”:“." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Namen dürfen nicht leer sein und kein „/“ oder „:“ enthalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los nombres no pueden estar vacíos ni contener “/” o “:”." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les noms ne peuvent pas être vides ni contenir « / » ou « : »." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "名前は空にできず、“/”や“:”を含めることはできません。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이름은 비워 둘 수 없으며 “/” 또는 “:”를 포함할 수 없습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "名称不能为空,也不能包含“/”或“:”。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "名稱不能為空白,也不能包含「/」或「:」。" + } + } + } + }, + "fileExplorer.operation.unsupported": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This action is only available for files on this Mac." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "هذا الإجراء متاح فقط للملفات الموجودة على هذا الـ Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Aktion ist nur für Dateien auf diesem Mac verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta acción solo está disponible para archivos de este Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette action n’est disponible que pour les fichiers de ce Mac." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "この操作はこのMac上のファイルでのみ使用できます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 동작은 이 Mac의 파일에서만 사용할 수 있습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此操作仅适用于这台 Mac 上的文件。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此動作僅適用於這台 Mac 上的檔案。" + } + } + } + }, + "fileSearch.action.clear": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear Search Results" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مسح نتائج البحث" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Suchergebnisse löschen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Borrar resultados de búsqueda" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer les résultats de recherche" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "検索結果をクリア" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "검색 결과 지우기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "清除搜索结果" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "清除搜尋結果" + } + } + } + }, + "fileSearch.action.collapseAll": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Collapse All" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "طي الكل" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle einklappen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Contraer todo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout réduire" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "すべて折りたたむ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "모두 접기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "全部折叠" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "全部摺疊" + } + } + } + }, + "fileSearch.action.dismissFile": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dismiss" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تجاهل" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descartar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "閉じる" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "닫기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "关闭" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "關閉" + } + } + } + }, + "fileSearch.action.expandAll": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Expand All" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "توسيع الكل" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle ausklappen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Expandir todo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout développer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "すべて展開" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "모두 펼치기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "全部展开" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "全部展開" + } + } + } + }, + "fileSearch.action.refresh": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Refresh" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تحديث" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktualisieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "更新" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새로 고침" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "刷新" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新整理" + } + } + } + }, + "fileSearch.error.invalidRegex": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invalid regular expression" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعبير نمطي غير صالح" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ungültiger regulärer Ausdruck" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Expresión regular no válida" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Expression régulière non valide" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "無効な正規表現" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "잘못된 정규 표현식" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无效的正则表达式" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無效的正規表示式" + } + } + } + }, + "fileSearch.error.rgMissingCloud": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) is not installed on this Cloud VM. Install it there to search this folder." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) غير مثبّت على هذا الجهاز الافتراضي السحابي. ثبّته عليه للبحث في هذا المجلد." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) ist auf dieser Cloud-VM nicht installiert. Installiere es dort, um diesen Ordner zu durchsuchen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) no está instalado en esta VM de Cloud. Instálalo allí para buscar en esta carpeta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) n’est pas installé sur cette VM Cloud. Installez-le sur celle-ci pour rechercher dans ce dossier." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "この Cloud VM に ripgrep (rg) がインストールされていません。このフォルダを検索するには VM にインストールしてください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 Cloud VM에 ripgrep(rg)이 설치되어 있지 않습니다. 이 폴더를 검색하려면 해당 VM에 설치하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此 Cloud VM 未安装 ripgrep (rg)。请在其上安装以搜索此文件夹。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此 Cloud VM 未安裝 ripgrep (rg)。請在其上安裝以搜尋此資料夾。" + } + } + } + }, + "fileSearch.error.rgMissingSSH": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) is not installed on %@. Install it there to search this folder." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) غير مثبّت على %@. ثبّته عليه للبحث في هذا المجلد." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) ist auf %@ nicht installiert. Installiere es dort, um diesen Ordner zu durchsuchen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) no está instalado en %@. Instálalo allí para buscar en esta carpeta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ripgrep (rg) n’est pas installé sur %@. Installez-le sur cet hôte pour rechercher dans ce dossier." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%@ に ripgrep (rg) がインストールされていません。このフォルダを検索するにはホストにインストールしてください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%@에 ripgrep(rg)이 설치되어 있지 않습니다. 이 폴더를 검색하려면 해당 호스트에 설치하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "%@ 未安装 ripgrep (rg)。请在其上安装以搜索此文件夹。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "%@ 未安裝 ripgrep (rg)。請在其上安裝以搜尋此資料夾。" + } + } + } + }, + "fileSearch.error.sshFailed": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not search over SSH. Check the connection and try again." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّر البحث عبر SSH. تحقّق من الاتصال وحاول مرة أخرى." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Suche über SSH ist fehlgeschlagen. Prüfe die Verbindung und versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo buscar por SSH. Comprueba la conexión e inténtalo de nuevo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de rechercher via SSH. Vérifiez la connexion et réessayez." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "SSH 経由で検索できませんでした。接続を確認してもう一度お試しください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "SSH로 검색할 수 없습니다. 연결을 확인한 후 다시 시도하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法通过 SSH 搜索。请检查连接后重试。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法透過 SSH 搜尋。請檢查連線後再試一次。" + } + } + } + }, + "fileSearch.error.sshFailedDetail": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not search over SSH: %@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّر البحث عبر SSH: %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Suche über SSH ist fehlgeschlagen: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo buscar por SSH: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de rechercher via SSH : %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "SSH 経由で検索できませんでした: %@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "SSH로 검색할 수 없습니다: %@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法通过 SSH 搜索:%@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法透過 SSH 搜尋:%@" + } + } + } + }, + "fileSearch.exclude.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Files to Exclude" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الملفات المستبعدة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auszuschließende Dateien" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos que excluir" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichiers à exclure" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "除外するファイル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "제외할 파일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "要排除的文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "要排除的檔案" + } + } + } + }, + "fileSearch.exclude.placeholder": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "files to exclude" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الملفات المستبعدة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "auszuschließende Dateien" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "archivos que excluir" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fichiers à exclure" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "除外するファイル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "제외할 파일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "要排除的文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "要排除的檔案" + } + } + } + }, + "fileSearch.include.label": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Files to Include" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الملفات المضمّنة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einzuschließende Dateien" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos que incluir" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichiers à inclure" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "含めるファイル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "포함할 파일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "要包含的文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "要包含的檔案" + } + } + } + }, + "fileSearch.include.placeholder": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "files to include" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الملفات المضمّنة" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "einzuschließende Dateien" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "archivos que incluir" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fichiers à inclure" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "含めるファイル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "포함할 파일" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "要包含的文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "要包含的檔案" + } + } + } + }, + "fileSearch.matchRow.accessibility": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Line %1$@: %2$@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "السطر %1$@: %2$@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeile %1$@: %2$@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Línea %1$@: %2$@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ligne %1$@ : %2$@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%1$@ 行目: %2$@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%1$@행: %2$@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "第 %1$@ 行:%2$@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "第 %1$@ 行:%2$@" + } + } + } + }, + "fileSearch.status.fileCount": { + "extractionState": "manual", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld file" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld files" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Datei" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Dateien" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld fichier" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%lld de fichiers" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld fichiers" + } + } + } + } + }, + "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": "%lld archivo" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%lld de archivos" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld archivos" + } + } + } + } + }, + "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 個のファイル" + } + } + } + } + } + } + }, + "fileSearch.status.limited": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@. Search limited to the first %@ results; refine the search to see more." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "%@. اقتصر البحث على أول %@ من النتائج؛ حسّن البحث لرؤية المزيد." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@. Suche auf die ersten %@ Ergebnisse begrenzt; verfeinere die Suche, um mehr zu sehen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@. Búsqueda limitada a los primeros %@ resultados; refina la búsqueda para ver más." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@. Recherche limitée aux %@ premiers résultats ; affinez la recherche pour en voir davantage." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%@。検索は最初の %@ 件の結果に制限されています。さらに表示するには検索条件を絞り込んでください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%@. 검색이 처음 %@개 결과로 제한되었습니다. 더 보려면 검색을 구체화하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "%@。搜索仅限前 %@ 个结果;请缩小搜索范围以查看更多。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "%@。搜尋僅限前 %@ 個結果;請縮小搜尋範圍以查看更多。" + } + } + } + }, + "fileSearch.status.noResults": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No results found" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يتم العثور على نتائج" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Ergebnisse gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se encontraron resultados" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun résultat trouvé" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "結果が見つかりません" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "결과 없음" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "未找到结果" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "找不到結果" + } + } + } + }, + "fileSearch.status.resultCount": { + "extractionState": "manual", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld result" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld results" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Ergebnis" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Ergebnisse" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld résultat" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%lld de résultats" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld résultats" + } + } + } + } + }, + "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": "%lld resultado" + } + }, + "many": { + "stringUnit": { + "state": "translated", + "value": "%lld de resultados" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld resultados" + } + } + } + } + }, + "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 件の結果" + } + } + } + } + } + } + }, + "fileSearch.status.searching": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Searching…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ البحث…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Suche läuft…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "検索中…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "검색 중…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在搜索…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在搜尋…" + } + } + } + }, + "fileSearch.status.searchingWithResults": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@, searching…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "%@، جارٍ البحث…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@, Suche läuft…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@, buscando…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@, recherche…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%@、検索中…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%@, 검색 중…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "%@,正在搜索…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "%@,正在搜尋…" + } + } + } + }, + "fileSearch.status.summary": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ in %2$@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "%1$@ في %2$@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%1$@ in %2$@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%1$@ en %2$@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$@ dans %2$@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%1$@(%2$@)" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%1$@(%2$@)" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "%1$@,位于 %2$@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "%1$@,位於 %2$@" + } + } + } + }, + "fileSearch.status.unsupported": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search is not available for this folder" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "البحث غير متاح لهذا المجلد" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Suche ist für diesen Ordner nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La búsqueda no está disponible para esta carpeta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La recherche n’est pas disponible pour ce dossier" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このフォルダでは検索を利用できません" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 폴더에서는 검색을 사용할 수 없습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此文件夹无法使用搜索" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此資料夾無法使用搜尋" + } + } + } + }, + "fileSearch.toggle.details": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toggle Search Details" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار/إخفاء تفاصيل البحث" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Suchdetails ein-/ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar u ocultar detalles de búsqueda" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher/masquer les détails de recherche" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "検索の詳細を切り替え" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "검색 세부 정보 전환" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "切换搜索详细信息" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "切換搜尋詳細資料" + } + } + } + }, + "fileSearch.toggle.matchCase": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Match Case" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مطابقة حالة الأحرف" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Groß-/Kleinschreibung beachten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Coincidir mayúsculas y minúsculas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Respecter la casse" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "大文字と小文字を区別する" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "대/소문자 구분" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "区分大小写" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "大小寫須相符" + } + } + } + }, + "fileSearch.toggle.regex": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use Regular Expression" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدام تعبير نمطي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Regulären Ausdruck verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar expresión regular" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser une expression régulière" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "正規表現を使用する" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "정규식 사용" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "使用正则表达式" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "使用規則運算式" + } + } + } + }, + "fileSearch.toggle.useIgnoreFiles": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use Exclude Settings and Ignore Files" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدام إعدادات الاستبعاد وملفات التجاهل" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausschlusseinstellungen und Ignorier-Dateien verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar configuración de exclusión y archivos de omisión" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser les paramètres d’exclusion et les fichiers d’exclusion" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "除外設定と無視ファイルを使用する" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "제외 설정 및 무시 파일 사용" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "使用排除设置和忽略文件" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "使用排除設定與忽略檔案" + } + } + } + }, + "fileSearch.toggle.wholeWord": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Match Whole Word" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مطابقة الكلمة بأكملها" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur ganzes Wort suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo palabras completas" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mot entier uniquement" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "単語単位で検索する" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "단어 단위로" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "全字匹配" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "全字拼寫須相符" + } + } + } + }, + "fileSearch.action.collapseFile": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Collapse" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "طي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einklappen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Contraer" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réduire" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "折りたたむ" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "접기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "折叠" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "摺疊" + } + } + } + }, + "fileSearch.action.expandFile": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Expand" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "توسيع" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausklappen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Expandir" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Développer" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "展開" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "펼치기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "展开" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "展開" + } + } + } + }, + "cli.rightSidebar.error.findOptionsRequireFind": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query, --regex, --case-sensitive and --whole-word need 'find' or 'set find' and a --query" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: تتطلب --query و--regex و--case-sensitive و--whole-word الأمر 'find' أو 'set find' مع --query" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query, --regex, --case-sensitive und --whole-word erfordern 'find' oder 'set find' und ein --query" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query, --regex, --case-sensitive y --whole-word requieren 'find' o 'set find' y un --query" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar : --query, --regex, --case-sensitive et --whole-word nécessitent 'find' ou 'set find' et un --query" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query、--regex、--case-sensitive、--whole-word には 'find' または 'set find' と --query が必要です" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query, --regex, --case-sensitive, --whole-word에는 'find' 또는 'set find'와 --query가 필요합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar:--query、--regex、--case-sensitive 和 --whole-word 需要配合 'find' 或 'set find' 以及 --query" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar:--query、--regex、--case-sensitive 和 --whole-word 需要搭配 'find' 或 'set find' 以及 --query" + } + } + } + }, + "cli.rightSidebar.error.queryRequiresValue": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query requires text" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: يتطلب --query نصًا" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query erfordert Text" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query requiere texto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar : --query nécessite un texte" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query にはテキストが必要です" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar: --query에는 텍스트가 필요합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar:--query 需要文本" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "right-sidebar:--query 需要文字" + } + } + } + }, + "cli.rightSidebar.usage.find": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Find:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n Search Find for as if typed into its field\n find-status Print the Find query, phase, results and files as JSON" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "البحث:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n البحث عن كما لو كُتب في حقل البحث\n find-status طباعة استعلام البحث والمرحلة والنتائج والملفات بتنسيق JSON" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Suche:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n Mit Suche nach suchen, als wäre es in das Suchfeld eingegeben\n find-status Suchanfrage, Phase, Treffer und Dateien als JSON ausgeben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n Buscar como si se escribiera en el campo Buscar\n find-status Imprimir la consulta, la fase, los resultados y los archivos como JSON" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher :\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n Rechercher comme s’il était saisi dans le champ Rechercher\n find-status Afficher la requête, la phase, les résultats et les fichiers en JSON" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "検索:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n 検索フィールドに入力したように を検索します\n find-status 検索クエリ、フェーズ、結果数、ファイル数を JSON で出力します" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "찾기:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n 찾기 필드에 입력한 것처럼 를 검색합니다\n find-status 찾기 쿼리, 단계, 결과, 파일 수를 JSON으로 출력합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "查找:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n 如同在查找字段中输入一样搜索 \n find-status 以 JSON 打印查找查询、阶段、结果与文件数" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "尋找:\n set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]\n 如同在尋找欄位中輸入一般搜尋 \n find-status 以 JSON 列印尋找查詢、階段、結果與檔案數" + } + } + } + }, + "rightSidebar.remote.error.findUnavailable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Find is not open in the right sidebar" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ERROR: البحث غير مفتوح في الشريط الجانبي الأيمن" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Die Suche ist in der rechten Seitenleiste nicht geöffnet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Buscar no está abierto en la barra lateral derecha" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Rechercher n’est pas ouvert dans la barre latérale droite" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 右サイドバーで検索が開いていません" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 오른쪽 사이드바에서 찾기가 열려 있지 않습니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 右侧边栏未打开查找" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 右側邊欄未開啟尋找" + } + } + } + }, + "rightSidebar.remote.error.queryRequiresValue": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query requires text" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ERROR: يتطلب --query نصًا" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query erfordert Text" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query requiere texto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query nécessite un texte" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query にはテキストが必要です" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query에는 텍스트가 필요합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query 需要文本" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "ERROR: --query 需要文字" + } + } + } + }, + "rightSidebar.remote.error.usage.findQuery": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Usage: right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ERROR: الاستخدام: right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Verwendung: right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Uso: right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Utilisation : right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 使用法: right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 사용법: right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 用法:right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 用法:right_sidebar set find --query [--regex] [--case-sensitive] [--whole-word] [--no-focus]" + } + } + } + }, + "rightSidebar.remote.error.usage.findStatus": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Usage: right_sidebar find_status [--workspace=] [--window=]" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ERROR: الاستخدام: right_sidebar find_status [--workspace=] [--window=]" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Verwendung: right_sidebar find_status [--workspace=] [--window=]" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Uso: right_sidebar find_status [--workspace=] [--window=]" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "ERROR: Utilisation : right_sidebar find_status [--workspace=] [--window=]" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 使用法: right_sidebar find_status [--workspace=] [--window=]" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 사용법: right_sidebar find_status [--workspace=] [--window=]" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 用法:right_sidebar find_status [--workspace=] [--window=]" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "ERROR: 用法:right_sidebar find_status [--workspace=] [--window=]" } } } diff --git a/Sources/App/ShortcutBareStartRouting.swift b/Sources/App/ShortcutBareStartRouting.swift index 0b04b55a8c1d..89a9183227f3 100644 --- a/Sources/App/ShortcutBareStartRouting.swift +++ b/Sources/App/ShortcutBareStartRouting.swift @@ -40,7 +40,7 @@ enum KeyboardShortcutBareStartCache { private extension KeyboardShortcutSettings.Action { var participatesInAppWideBareStartRouting: Bool { switch self { - case .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias: + case .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias, .fileExplorerQuickLook, .fileExplorerRenameSelection, .fileExplorerToggleHiddenFiles, .fileExplorerSelectParent: return false default: return true diff --git a/Sources/AppDelegate+DockShortcutRouting.swift b/Sources/AppDelegate+DockShortcutRouting.swift index be2c4f12a328..bb2e68327413 100644 --- a/Sources/AppDelegate+DockShortcutRouting.swift +++ b/Sources/AppDelegate+DockShortcutRouting.swift @@ -70,6 +70,10 @@ extension KeyboardShortcutSettings.Action { .cycleTextBoxSubmitAction, .fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias, + .fileExplorerQuickLook, + .fileExplorerRenameSelection, + .fileExplorerToggleHiddenFiles, + .fileExplorerSelectParent, .saveFilePreview, .toggleFileEditorWordWrap, .browserBack, .browserForward, diff --git a/Sources/AppDelegate+SimulatorShortcutRouting.swift b/Sources/AppDelegate+SimulatorShortcutRouting.swift index cba865ba6ce1..d4df57fea888 100644 --- a/Sources/AppDelegate+SimulatorShortcutRouting.swift +++ b/Sources/AppDelegate+SimulatorShortcutRouting.swift @@ -54,6 +54,10 @@ extension AppDelegate { && action != .resetWorkspaceTerminalFontSize && action != .fileExplorerOpenSelection && action != .fileExplorerOpenSelectionFinderAlias + && action != .fileExplorerQuickLook + && action != .fileExplorerRenameSelection + && action != .fileExplorerToggleHiddenFiles + && action != .fileExplorerSelectParent && !KeyboardShortcutSettings.Action.simulatorActions.contains(action) } } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index c56ff96b63e2..1341fd0da189 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1,4 +1,5 @@ import CmuxCloud +import CmuxFileTree import CmuxCloudTui import CmuxComputerUse import CmuxCloudMachines @@ -856,6 +857,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent var computerUseRuntimeService: ComputerUseRuntimeService? private(set) var browserDataImportCoordinator: BrowserDataImportCoordinator? weak var fileExplorerState: FileExplorerState? + /// Files tree expansion, selection and scroll per workspace root, shared + /// by every window's store so the state survives restarts. + let fileExplorerViewStateRepository = FileTreeViewStateRepository(defaults: .standard) weak var fullscreenControlsViewModel: TitlebarControlsViewModel? weak var sidebarSelectionState: SidebarSelectionState? var shortcutLayoutCharacterProvider: (UInt16, NSEvent.ModifierFlags) -> String? = KeyboardLayout.character(forKeyCode:modifierFlags:) @@ -7837,9 +7841,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent switch command { case .focus: requiresWindowFocus = true - case .setMode(_, let focus), .setCustomSidebar(_, let focus): + case .setMode(_, let focus), .setCustomSidebar(_, let focus), .setFindQuery(_, let focus): requiresWindowFocus = focus - case .toggle, .show, .hide, .getState: + case .toggle, .show, .hide, .getState, .getFindStatus: requiresWindowFocus = false } if requiresWindowFocus, !target.isActiveTarget, preferredWindow == nil { @@ -7925,6 +7929,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return .ok case .getState: return .state(.init(visible: state.isVisible, modeRawValue: state.rightSidebarRemoteModeRawValue)) + + case .setFindQuery(let query, let focus): + let modeResult = applyRightSidebarRemoteCommand(.setMode(.find, focus: focus), target: target) + guard modeResult == .ok else { return modeResult } + guard let coordinator = (context ?? preferredRegisteredMainWindowContext(preferredWindow: preferredWindow))?.keyboardFocusCoordinator else { + return .failure(String(localized: "rightSidebar.remote.error.unavailable", defaultValue: "ERROR: Right sidebar not available")) + } + // Applied now when Find is mounted, otherwise when it registers. + coordinator.applyFileSearchQuery(query) + return .ok + + case .getFindStatus: + guard let status = ((context ?? preferredRegisteredMainWindowContext(preferredWindow: preferredWindow))?.keyboardFocusCoordinator)? + .fileSearchStatus() else { + return .failure(String(localized: "rightSidebar.remote.error.findUnavailable", defaultValue: "ERROR: Find is not open in the right sidebar")) + } + return .findStatus(status) } } @@ -7939,7 +7960,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let target = RightSidebarRemoteTarget(windowId: context.windowId) let result = applyRightSidebarRemoteCommand(.setMode(.machines, focus: true), target: target) switch result { - case .ok, .state: + case .ok, .state, .findStatus: registry.reveal(instance: instance, windowID: context.windowId) case .failure: break @@ -18128,6 +18149,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private func canCurrentShortcutPreventStaleMenuSuppression(_ action: KeyboardShortcutSettings.Action) -> Bool { action != .fileExplorerOpenSelection && action != .fileExplorerOpenSelectionFinderAlias + && action != .fileExplorerQuickLook + && action != .fileExplorerRenameSelection + && action != .fileExplorerToggleHiddenFiles + && action != .fileExplorerSelectParent } private func isCloseShortcutAction(_ action: KeyboardShortcutSettings.Action) -> Bool { diff --git a/Sources/BonsplitConfiguration+RemoteTmuxEmbedded.swift b/Sources/BonsplitConfiguration+RemoteTmuxEmbedded.swift index 40b9b1410a5b..ed706b817186 100644 --- a/Sources/BonsplitConfiguration+RemoteTmuxEmbedded.swift +++ b/Sources/BonsplitConfiguration+RemoteTmuxEmbedded.swift @@ -18,6 +18,7 @@ extension BonsplitConfiguration { configuration.appearance.minimumPaneWidth = 1 configuration.appearance.minimumPaneHeight = 1 configuration.appearance.tabBarLeadingInset = 0 + configuration.appearance.tabBarTrailingInset = 0 configuration.appearance.enableAnimations = false configuration.appearance.splitButtons = configuration.appearance.splitButtons.filter { switch $0.action { diff --git a/Sources/CloudFileExplorerSearchQueue.swift b/Sources/CloudFileExplorerSearchQueue.swift index 05041ffb53a6..27ae5662d72c 100644 --- a/Sources/CloudFileExplorerSearchQueue.swift +++ b/Sources/CloudFileExplorerSearchQueue.swift @@ -1,17 +1,33 @@ +import CmuxFileSearch import Foundation +/// One Cloud search's matches and how it ended. +struct CloudFileSearchResult: Sendable { + let groups: [FileSearchFileMatches] + let completion: FileSearchCompletion +} + +/// One queued Cloud search and its waiting UI continuation. +struct CloudFileExplorerSearchRequest { + typealias Operation = @Sendable () async throws -> CloudFileSearchResult + + let id: UUID + let operation: Operation + var continuation: CheckedContinuation? +} + /// One guest command remains active until its result arrives. Only the newest /// pending query is retained; cancellation cannot turn into overlapping VM execs. actor CloudFileExplorerSearchQueue { - typealias Operation = @Sendable () async throws -> FileSearchSnapshot + typealias Operation = @Sendable () async throws -> CloudFileSearchResult private var active: CloudFileExplorerSearchRequest? private var pending: CloudFileExplorerSearchRequest? - func submit(_ operation: @escaping Operation) async throws -> FileSearchSnapshot { + func submit(_ operation: @escaping Operation) async throws -> CloudFileSearchResult { let id = UUID() return try await withTaskCancellationHandler { try Task.checkCancellation() - let result: FileSearchSnapshot = try await withCheckedThrowingContinuation { continuation in + let result: CloudFileSearchResult = try await withCheckedThrowingContinuation { continuation in // Register and observe cancellation without suspending between them. guard !Task.isCancelled else { continuation.resume(throwing: CancellationError()) @@ -47,14 +63,14 @@ actor CloudFileExplorerSearchQueue { // This task intentionally outlives cancellation of its UI caller: VM // exec has no cancel RPC. Holding the slot until completion bounds work. Task { - let result: Result + let result: Result do { result = .success(try await next.operation()) } catch { result = .failure(error) } finish(next.id, result: result) } } - private func finish(_ id: UUID, result: Result) { + private func finish(_ id: UUID, result: Result) { guard active?.id == id else { return } let continuation = active?.continuation active = nil diff --git a/Sources/CloudFileExplorerSearchRequest.swift b/Sources/CloudFileExplorerSearchRequest.swift deleted file mode 100644 index b52c13a2052d..000000000000 --- a/Sources/CloudFileExplorerSearchRequest.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// One queued Cloud search and its waiting UI continuation. -struct CloudFileExplorerSearchRequest { - typealias Operation = @Sendable () async throws -> FileSearchSnapshot - - let id: UUID - let operation: Operation - var continuation: CheckedContinuation? -} diff --git a/Sources/CloudFileExplorerService.swift b/Sources/CloudFileExplorerService.swift index c7adcad6ab0e..44631697897e 100644 --- a/Sources/CloudFileExplorerService.swift +++ b/Sources/CloudFileExplorerService.swift @@ -1,9 +1,12 @@ +import CmuxFileTree +import CmuxFileSearch import Foundation /// Runs bounded filesystem operations on one Cloud VM. actor CloudFileExplorerService { private static let maxSearchResults = 500 private static let maxPreviewBytes = 1_048_576 + private static let maxEntriesPerDirectory = 10_000 private let commandRunner: any CloudFileExplorerCommandRunning private let searchQueue = CloudFileExplorerSearchQueue() @@ -25,39 +28,84 @@ actor CloudFileExplorerService { return home } - /// Lists one remote directory without crossing the local filesystem boundary. - func listDirectory(vmID: String, path: String, showHidden: Bool) async throws -> [FileExplorerEntry] { + /// Lists remote directories in one guest exec without crossing the local + /// filesystem boundary. Each directory returns at most + /// ``maxEntriesPerDirectory`` entries; the rest are counted as omitted so + /// a huge `node_modules` shows a partial tree instead of an error. + func listDirectories(vmID: String, paths: [String]) async throws -> [String: Result] { + guard !paths.isEmpty else { return [:] } let script = #""" -import json, os, sys -path = sys.argv[1] -show_hidden = sys.argv[2] == "1" -entries = [] -with os.scandir(path) as directory: - for entry in directory: - if not show_hidden and entry.name.startswith("."): - continue - try: - is_directory = entry.is_dir(follow_symlinks=True) - except OSError: - is_directory = False - entries.append({"name": entry.name, "path": entry.path, "directory": is_directory}) - if len(entries) > 10000: - sys.exit(74) -json.dump(entries, sys.stdout, separators=(",", ":")) +import json, os, stat, sys +limit = int(sys.argv[1]) +results = [] +for path in sys.argv[2:]: + entries = [] + omitted = 0 + try: + with os.scandir(path) as directory: + for entry in directory: + if len(entries) >= limit: + omitted += 1 + continue + kind = "f" + size = None + mtime = None + try: + info = entry.stat(follow_symlinks=False) + mtime = info.st_mtime + if stat.S_ISDIR(info.st_mode): + kind = "d" + elif stat.S_ISLNK(info.st_mode): + kind = "L" if entry.is_dir(follow_symlinks=True) else "l" + elif stat.S_ISREG(info.st_mode): + size = info.st_size + else: + kind = "o" + except OSError: + kind = "o" + entries.append([entry.name, kind, size, mtime]) + results.append({"ok": True, "entries": entries, "omitted": omitted}) + except OSError as error: + results.append({"ok": False, "error": error.strerror or "error"}) +json.dump(results, sys.stdout, separators=(",", ":")) """# - let command = "python3 -c \(Self.shellQuote(script)) \(Self.shellQuote(path)) \(showHidden ? "1" : "0")" + let quotedPaths = paths.map(Self.shellQuote).joined(separator: " ") + let command = "python3 -c \(Self.shellQuote(script)) \(Self.maxEntriesPerDirectory) \(quotedPaths)" let result = try await commandRunner.run(vmID: vmID, command: command, timeoutMs: 30_000) guard result.exitCode == 0, let data = result.stdout.data(using: .utf8), - let rawEntries = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { + let rawResults = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]], + rawResults.count == paths.count else { throw FileExplorerError.remoteCommandFailed("") } - return rawEntries.compactMap { raw in - guard let name = raw["name"] as? String, - let entryPath = raw["path"] as? String, - let isDirectory = raw["directory"] as? Bool else { return nil } - return FileExplorerEntry(name: name, path: entryPath, isDirectory: isDirectory) + var listings: [String: Result] = [:] + for (path, raw) in zip(paths, rawResults) { + guard raw["ok"] as? Bool == true, let rawEntries = raw["entries"] as? [[Any]] else { + listings[path] = .failure(FileExplorerError.remoteCommandFailed((raw["error"] as? String) ?? "")) + continue + } + let parent = path.hasSuffix("/") ? path : path + "/" + let entries = rawEntries.compactMap { fields -> FileTreeEntry? in + guard fields.count == 4, let name = fields[0] as? String, let code = fields[1] as? String else { return nil } + let kind: FileTreeEntryKind + switch code { + case "d": kind = .directory + case "L": kind = .symbolicLinkToDirectory + case "l": kind = .symbolicLink + case "o": kind = .other + default: kind = .file + } + return FileTreeEntry( + name: name, + path: parent + name, + kind: kind, + size: (fields[2] as? NSNumber)?.int64Value, + modificationTime: (fields[3] as? NSNumber)?.doubleValue + ) + } + listings[path] = .success(FileTreeListing(entries: entries, omittedCount: (raw["omitted"] as? Int) ?? 0)) } + return listings } /// Downloads one bounded remote file to a local preview cache. @@ -99,28 +147,37 @@ sys.stdout.write(base64.b64encode(data).decode("ascii")) } /// Keeps canceled HTTP callers from spawning overlapping guest scans. - func search(vmID: String, query: String, rootPath: String) async throws -> FileSearchSnapshot { + func search(vmID: String, query: FileSearchQuery, rootPath: String, matchLimit: Int) async throws -> CloudFileSearchResult { let runner = commandRunner return try await searchQueue.submit { - try await Self.performSearch(commandRunner: runner, vmID: vmID, query: query, rootPath: rootPath) + try await Self.performSearch( + commandRunner: runner, + vmID: vmID, + query: query, + rootPath: rootPath, + matchLimit: matchLimit + ) } } + /// The exec API returns stdout only when the command ends, so the guest + /// filter keeps match lines only and stops ripgrep at a line or byte + /// budget. Exit 75 means `rg` is not installed on the VM. private static func performSearch( commandRunner: any CloudFileExplorerCommandRunning, vmID: String, - query: String, - rootPath: String - ) async throws -> FileSearchSnapshot { + query: FileSearchQuery, + rootPath: String, + matchLimit: Int + ) async throws -> CloudFileSearchResult { + let lineLimit = min(matchLimit, maxSearchResults) let script = #""" import subprocess, sys -limit = \#(Self.maxSearchResults) +limit = int(sys.argv[1]) byte_limit = \#(Self.maxPreviewBytes) -query = sys.argv[1] -root = sys.argv[2] -rg_args = sys.argv[3:] +rg_args = sys.argv[2:] try: - process = subprocess.Popen(["rg", *rg_args, "--", query, root], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + process = subprocess.Popen(["rg", *rg_args], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: sys.exit(75) count = 0 @@ -145,39 +202,32 @@ if limited: except subprocess.TimeoutExpired: process.kill() process.wait() - sys.stdout.buffer.write(f"__CMUX_LIMIT__:{count}\n".encode()) + sys.stdout.buffer.write(b"__CMUX_LIMIT__\n") sys.stdout.buffer.flush() sys.exit(0) exit_code = process.wait() +if exit_code not in (0, 1): + sys.stderr.buffer.write(process.stderr.read()[-4096:]) sys.exit(0 if exit_code in (0, 1) else exit_code) """# - let rgArguments = [ - "--json", "--line-number", "--column", "--smart-case", "--fixed-strings", - "--max-columns", "300", "--max-columns-preview", "--color", "never", "--hidden", - "--glob", "!.git/**", "--glob", "!**/.git/**", "--glob", "!node_modules/**", - "--glob", "!**/node_modules/**", "--glob", "!dist/**", "--glob", "!**/dist/**", - "--glob", "!build/**", "--glob", "!**/build/**", "--glob", "!DerivedData/**", - "--glob", "!**/DerivedData/**", - ] - let command = "python3 -c \(Self.shellQuote(script)) \(Self.shellQuote(query)) \(Self.shellQuote(rootPath)) " - + rgArguments.map(Self.shellQuote).joined(separator: " ") + let command = "python3 -c \(Self.shellQuote(script)) \(lineLimit) " + + query.ripgrepArguments(rootPath: rootPath).map(Self.shellQuote).joined(separator: " ") let result = try await commandRunner.run(vmID: vmID, command: command, timeoutMs: 30_000) - let limitCount = result.stdout - .split(whereSeparator: \.isNewline) - .first(where: { $0.hasPrefix("__CMUX_LIMIT__:") }) - .flatMap { Int($0.dropFirst("__CMUX_LIMIT__:".count)) } - let results = result.stdout - .split(whereSeparator: \.isNewline) - .compactMap { FileSearchRipgrepParser.parseMatchLine(String($0), rootPath: rootPath) } - guard result.exitCode == 0 || result.exitCode == 1 else { - throw FileExplorerError.remoteCommandFailed("") + if result.exitCode == 75 { + return CloudFileSearchResult(groups: [], completion: .failed(.ripgrepNotFound)) } - return FileSearchSnapshot( - query: query, - results: results, - status: results.isEmpty ? .noMatches : (limitCount.map { .limited($0) } ?? .matches), - isSearching: false + let decoder = RipgrepStreamDecoder(matchLimit: Int.max) + var groups = decoder.consume(Array(result.stdout.utf8)) + groups.appendMerging(decoder.finish()) + let wasLimited = result.stdout.contains("__CMUX_LIMIT__") + let completion = FileSearchCompletion( + ripgrepExitStatus: Int32(truncatingIfNeeded: result.exitCode), + standardError: result.stderr, + matchCount: decoder.matchCount, + limitReached: wasLimited, + matchLimit: decoder.matchCount ) + return CloudFileSearchResult(groups: groups, completion: completion) } private static func shellQuote(_ value: String) -> String { diff --git a/Sources/CloudVMFileExplorerProvider.swift b/Sources/CloudVMFileExplorerProvider.swift index 7ce3e23ac3a8..06cb4680e70a 100644 --- a/Sources/CloudVMFileExplorerProvider.swift +++ b/Sources/CloudVMFileExplorerProvider.swift @@ -1,3 +1,5 @@ +import CmuxFileTree +import CmuxFileSearch import Foundation /// An immutable Cloud filesystem identity with I/O owned by its service actor. @@ -56,14 +58,29 @@ final class CloudVMFileExplorerProvider: RemoteFileExplorerProvider, Sendable { } /// Lists a directory on the Cloud machine. - nonisolated func listDirectory(path: String, showHidden: Bool) async throws -> [FileExplorerEntry] { - guard isAvailable else { throw FileExplorerError.providerUnavailable } - return try await service.listDirectory(vmID: vmID, path: path, showHidden: showHidden) + nonisolated func listDirectory(at path: String) async throws -> FileTreeListing { + guard let result = await listDirectories(at: [path])[path] else { + throw FileExplorerError.providerUnavailable + } + return try result.get() + } + + /// Lists several directories in one guest exec. + nonisolated func listDirectories(at paths: [String]) async -> [String: Result] { + guard isAvailable else { + return Dictionary(uniqueKeysWithValues: paths.map { ($0, .failure(FileExplorerError.providerUnavailable)) }) + } + do { + return try await service.listDirectories(vmID: vmID, paths: paths) + } catch { + return Dictionary(uniqueKeysWithValues: paths.map { ($0, .failure(error)) }) + } } - nonisolated func search(query: String, rootPath: String) async throws -> FileSearchSnapshot { + /// Searches the Cloud machine with ripgrep. + nonisolated func search(query: FileSearchQuery, rootPath: String, matchLimit: Int) async throws -> CloudFileSearchResult { guard isAvailable else { throw FileExplorerError.providerUnavailable } - return try await service.search(vmID: vmID, query: query, rootPath: rootPath) + return try await service.search(vmID: vmID, query: query, rootPath: rootPath, matchLimit: matchLimit) } /// Downloads a remote file into the local preview cache. diff --git a/Sources/CmuxSettingsFileStore+SupportedPaths.swift b/Sources/CmuxSettingsFileStore+SupportedPaths.swift index 5a35aef5dc84..feb29080d470 100644 --- a/Sources/CmuxSettingsFileStore+SupportedPaths.swift +++ b/Sources/CmuxSettingsFileStore+SupportedPaths.swift @@ -173,6 +173,7 @@ extension CmuxSettingsFileStore { "fileEditor.currentLineHighlight", "fileEditor.tabWidth", "fileExplorer.doubleClickAction", + "rightSidebar.toggleButton", "shortcuts.bindings", "shortcuts.showModifierHoldHints", ] diff --git a/Sources/ContentView+RightSidebarToggleButtonCommandPalette.swift b/Sources/ContentView+RightSidebarToggleButtonCommandPalette.swift new file mode 100644 index 000000000000..c0e75a50347c --- /dev/null +++ b/Sources/ContentView+RightSidebarToggleButtonCommandPalette.swift @@ -0,0 +1,58 @@ +import CmuxCommandPalette +import CmuxSettings +import Foundation + +/// Command palette entries that switch `rightSidebar.toggleButton` live. +/// +/// Each command writes the same UserDefaults key that `cmux.json` manages, so +/// every window updates at once. A later edit of `cmux.json` wins again. +extension RightSidebarToggleButtonPlacement { + var commandPaletteCommandId: String { + "palette.rightSidebarToggleButton.\(rawValue)" + } + + /// Localized name of the placement, shared by the palette and debug menu. + var localizedTitle: String { + switch self { + case .titlebar: + return String(localized: "rightSidebar.toggleButton.placement.titlebar", defaultValue: "Window Corner") + case .paneTabBar: + return String(localized: "rightSidebar.toggleButton.placement.paneTabBar", defaultValue: "Pane Tab Bar") + case .sidebarFooter: + return String(localized: "rightSidebar.toggleButton.placement.sidebarFooter", defaultValue: "Left Sidebar Footer") + case .hidden: + return String(localized: "rightSidebar.toggleButton.placement.hidden", defaultValue: "Hidden") + } + } + + /// Makes this placement the active one for every window. + func apply(defaults: UserDefaults = .standard) { + SettingCatalog().rightSidebar.toggleButton.set(self, in: defaults) + } +} + +extension ContentView { + static func commandPaletteRightSidebarToggleButtonContributions() -> [CommandPaletteCommandContribution] { + RightSidebarToggleButtonPlacement.allCases.map { placement in + let title = String.localizedStringWithFormat( + String(localized: "command.rightSidebarToggleButton.title", defaultValue: "Right Sidebar Button: %@"), + placement.localizedTitle + ) + let subtitle = String(localized: "command.rightSidebarToggleButton.subtitle", defaultValue: "View") + return CommandPaletteCommandContribution( + commandId: placement.commandPaletteCommandId, + title: { _ in title }, + subtitle: { _ in subtitle }, + keywords: ["right", "sidebar", "toggle", "button", "placement", "titlebar", "corner", "tab bar", "footer", "hide"] + ) + } + } + + func registerRightSidebarToggleButtonCommandHandlers(_ registry: inout CommandPaletteHandlerRegistry) { + for placement in RightSidebarToggleButtonPlacement.allCases { + registry.register(commandId: placement.commandPaletteCommandId) { + placement.apply() + } + } + } +} diff --git a/Sources/ContentView+TitlebarLayout.swift b/Sources/ContentView+TitlebarLayout.swift new file mode 100644 index 000000000000..460c9afeb834 --- /dev/null +++ b/Sources/ContentView+TitlebarLayout.swift @@ -0,0 +1,61 @@ +import CoreGraphics + +extension ContentView { + static func effectiveTitlebarPadding( + isMinimalMode: Bool, + isFullScreen: Bool, + titlebarPadding: CGFloat, + hostingSafeAreaTop: CGFloat + ) -> CGFloat { + guard isMinimalMode else { return WindowChromeMetrics.appTitlebarHeight } + guard !isFullScreen else { return 0 } + return -max(0, min(titlebarPadding, hostingSafeAreaTop)) + } + + nonisolated static func customTitlebarLeadingPadding( + isFullScreen: Bool, + isSidebarVisible: Bool, + sidebarWidth: CGFloat, + minimumSidebarWidth: CGFloat, + titlebarLeadingInset: CGFloat + ) -> CGFloat { + if isFullScreen && !isSidebarVisible { + return 8 + } + + let minimumSidebarTitleInset = max(titlebarLeadingInset, minimumSidebarWidth + 12) + guard isSidebarVisible else { + return minimumSidebarTitleInset + } + + let visibleSidebarTitleInset = sidebarWidth + 12 + // Absorb floating-point drift around the minimum-width clamp. + guard sidebarWidth > minimumSidebarWidth + 0.5 else { + return minimumSidebarTitleInset + } + return max(titlebarLeadingInset, visibleSidebarTitleInset) + } + + /// Where the always-visible fullscreen titlebar controls (sidebar toggle, + /// history, new tab, notifications) are anchored inside the titlebar band. + struct FullscreenControlsPlacement: Equatable { + var leadingPadding: CGFloat + var topPadding: CGFloat + } + + /// Resolves the placement for the fullscreen titlebar controls, or `nil` when + /// they should not be shown. The controls are mounted in a single overlay + /// anchor driven by this function so their on-screen position never depends on + /// sidebar visibility; toggling the sidebar must not shift the accessory bar. + nonisolated static func fullscreenControlsPlacement( + isFullScreen: Bool, + isSidebarVisible: Bool + ) -> FullscreenControlsPlacement? { + guard isFullScreen else { return nil } + // Placement is intentionally independent of sidebar visibility so toggling + // the sidebar in fullscreen never shifts the accessory bar. `topPadding` + // mirrors the title row's top inset (see `customTitlebar`) so the controls' + // center lines up with the folder icon / title. + return FullscreenControlsPlacement(leadingPadding: 10, topPadding: 2) + } +} diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 61e6ad99ddd1..66243ebe1b89 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1,5 +1,6 @@ import CmuxCloud import AppKit +import CmuxFileTree import CmuxAppKitSupportUI import CmuxCommandPalette import CmuxCore @@ -938,7 +939,9 @@ struct ContentView: View { @State private var workspaceSwitchPortalSignalRouter = WorkspaceSwitchPortalSignalRouter() @State private var sidebarRenderWorkerClient: RenderWorkerClient? @StateObject private var fullscreenControlsViewModel = TitlebarControlsViewModel() - @StateObject private var fileExplorerStore = FileExplorerStore() + @StateObject private var fileExplorerStore = FileExplorerStore( + viewStateRepository: AppDelegate.shared?.fileExplorerViewStateRepository + ) @StateObject private var sessionIndexStore = SessionIndexStore() @StateObject private var selectedWorkspaceDirectoryObserver = SelectedWorkspaceDirectoryObserver() @State private var commandPaletteOverlayRenderModel = CommandPaletteOverlayRenderModel() @@ -1859,64 +1862,6 @@ struct ContentView: View { workspacePresentationModeRuntimeCache.isMinimalMode } - static func effectiveTitlebarPadding( - isMinimalMode: Bool, - isFullScreen: Bool, - titlebarPadding: CGFloat, - hostingSafeAreaTop: CGFloat - ) -> CGFloat { - guard isMinimalMode else { return WindowChromeMetrics.appTitlebarHeight } - guard !isFullScreen else { return 0 } - return -max(0, min(titlebarPadding, hostingSafeAreaTop)) - } - - nonisolated static func customTitlebarLeadingPadding( - isFullScreen: Bool, - isSidebarVisible: Bool, - sidebarWidth: CGFloat, - minimumSidebarWidth: CGFloat, - titlebarLeadingInset: CGFloat - ) -> CGFloat { - if isFullScreen && !isSidebarVisible { - return 8 - } - - let minimumSidebarTitleInset = max(titlebarLeadingInset, minimumSidebarWidth + 12) - guard isSidebarVisible else { - return minimumSidebarTitleInset - } - - let visibleSidebarTitleInset = sidebarWidth + 12 - // Absorb floating-point drift around the minimum-width clamp. - guard sidebarWidth > minimumSidebarWidth + 0.5 else { - return minimumSidebarTitleInset - } - return max(titlebarLeadingInset, visibleSidebarTitleInset) - } - - /// Where the always-visible fullscreen titlebar controls (sidebar toggle, - /// history, new tab, notifications) are anchored inside the titlebar band. - struct FullscreenControlsPlacement: Equatable { - var leadingPadding: CGFloat - var topPadding: CGFloat - } - - /// Resolves the placement for the fullscreen titlebar controls, or `nil` when - /// they should not be shown. The controls are mounted in a single overlay - /// anchor driven by this function so their on-screen position never depends on - /// sidebar visibility; toggling the sidebar must not shift the accessory bar. - nonisolated static func fullscreenControlsPlacement( - isFullScreen: Bool, - isSidebarVisible: Bool - ) -> FullscreenControlsPlacement? { - guard isFullScreen else { return nil } - // Placement is intentionally independent of sidebar visibility so toggling - // the sidebar in fullscreen never shifts the accessory bar. `topPadding` - // mirrors the title row's top inset (see `customTitlebar`) so the controls' - // center lines up with the folder icon / title. - return FullscreenControlsPlacement(leadingPadding: 10, topPadding: 2) - } - private func terminalContent(appearance: WindowAppearanceSnapshot) -> some View { let selectedWorkspaceId = tabManager.selectedTabId // Selection reaches body before onChange reconciles the mount cache. @@ -1968,6 +1913,11 @@ struct ContentView: View { .opacity(sidebarSelectionState.selection == .tabs ? 1 : 0) .allowsHitTesting(sidebarSelectionState.selection == .tabs) .accessibilityHidden(sidebarSelectionState.selection != .tabs) + RightSidebarTogglePaneTabBarHost( + fileExplorerState: fileExplorerState, + isWorkspaceContentShown: sidebarSelectionState.selection == .tabs + && tabManager.selectedWorkspace?.layoutMode != .canvas + ) } .modifier(WorkspacePresentationModeContentTopPaddingModifier( isFullScreen: isFullScreen, @@ -2272,7 +2222,8 @@ struct ContentView: View { minimumSidebarWidth: minimumSidebarWidth, titlebarLeadingInset: titlebarLeadingInset )) - .padding(.trailing, 8) + // Keep the title clear of the corner right-sidebar button. + .padding(.trailing, 8 + RightSidebarToggleButtonLayout.tabBarLaneWidth) } } .frame(height: WindowChromeMetrics.appTitlebarHeight) @@ -2528,7 +2479,8 @@ struct ContentView: View { return } - fileExplorerStore.showHiddenFiles = true + fileExplorerStore.showHiddenFiles = fileExplorerState.showHiddenFiles + fileExplorerStore.sortOrder = fileExplorerState.sortOrder let directory = tab.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines) sessionIndexStore.setCurrentDirectoryIfChanged( @@ -2716,6 +2668,8 @@ struct ContentView: View { workspaceTitlebarBand(appearance: appearance) .zIndex(100) } + RightSidebarToggleWindowChrome(fileExplorerState: fileExplorerState, tabManager: tabManager) + .zIndex(101) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(minWidth: CGFloat(SessionPersistencePolicy.minimumWindowWidth), minHeight: CGFloat(SessionPersistencePolicy.minimumWindowHeight)) @@ -7714,6 +7668,7 @@ struct ContentView: View { ) ) contributions.append(contentsOf: Self.commandPaletteViewCommandContributions()) + contributions.append(contentsOf: Self.commandPaletteRightSidebarToggleButtonContributions()) contributions.append(contentsOf: Self.commandPaletteCanvasCommandContributions()) contributions.append( CommandPaletteCommandContribution( @@ -9007,6 +8962,7 @@ struct ContentView: View { ) } registerViewCommandHandlers(®istry) + registerRightSidebarToggleButtonCommandHandlers(®istry) registerCanvasCommandHandlers(®istry) registerCloudCommandHandlers(®istry) registerComputerUseCommandPaletteHandlers(®istry) @@ -15628,6 +15584,8 @@ struct SidebarFooterButtons: View { if shows(.update), let updateActionsHost = AppDelegate.shared { UpdatePill(model: updateViewModel, accent: cmuxAccent.color, actions: updateActionsHost) } + Spacer(minLength: 0) + RightSidebarToggleSidebarFooterHost(fileExplorerState: fileExplorerState) } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Sources/FileExplorerCellView.swift b/Sources/FileExplorerCellView.swift index f4b95742c302..296e67a2cf40 100644 --- a/Sources/FileExplorerCellView.swift +++ b/Sources/FileExplorerCellView.swift @@ -1,15 +1,28 @@ import AppKit -import CmuxAppKitSupportUI -import UniformTypeIdentifiers +/// One reusable Files row: icon, name, git badge and an on-demand spinner. +/// +/// Cells are recycled by `NSOutlineView`; `configure` only writes values that +/// changed so scrolling does not invalidate layout, and icons come from the +/// shared ``FileExplorerIconCache``. final class FileExplorerCellView: NSTableCellView { - private let iconView = CmuxResolvedIconImageView() + private let iconView = NSImageView() private let nameLabel = NSTextField(labelWithString: "") - private let loadingIndicator = NSProgressIndicator() + private let badgeLabel = NSTextField(labelWithString: "") + private var loadingIndicator: NSProgressIndicator? private var trackingArea: NSTrackingArea? - var onHover: ((Bool) -> Void)? - private var nameLabelTrailingToLoadingConstraint: NSLayoutConstraint! - private var nameLabelTrailingToContainerConstraint: NSLayoutConstraint! + var onHover: ((Bool) -> Void)? { + didSet { + if (onHover == nil) != (oldValue == nil) { updateTrackingAreas() } + } + } + private var iconWidthConstraint: NSLayoutConstraint! + private var iconHeightConstraint: NSLayoutConstraint! + private var iconToTextConstraint: NSLayoutConstraint! + private var badgeTrailingConstraint: NSLayoutConstraint! + private(set) weak var node: FileExplorerNode? + private var renderedIconKey: (style: FileExplorerStyle, appearance: NSAppearance.Name?, nodeKey: String)? + private weak var iconCache: FileExplorerIconCache? init(identifier: NSUserInterfaceItemIdentifier) { super.init(frame: .zero) @@ -21,138 +34,230 @@ final class FileExplorerCellView: NSTableCellView { fatalError("init(coder:) has not been implemented") } - private var iconWidthConstraint: NSLayoutConstraint! - private var iconHeightConstraint: NSLayoutConstraint! - private var iconToTextConstraint: NSLayoutConstraint! - private var loadingWidthConstraint: NSLayoutConstraint! - private func setupViews() { iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.imageScaling = .scaleProportionallyDown + iconView.animates = false + imageView = iconView nameLabel.translatesAutoresizingMaskIntoConstraints = false nameLabel.textColor = .labelColor - nameLabel.lineBreakMode = .byTruncatingTail + nameLabel.lineBreakMode = .byTruncatingMiddle nameLabel.maximumNumberOfLines = 1 + nameLabel.cell?.usesSingleLineMode = true + nameLabel.cell?.isScrollable = false + nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + textField = nameLabel - loadingIndicator.translatesAutoresizingMaskIntoConstraints = false - loadingIndicator.style = .spinning - loadingIndicator.controlSize = .small - loadingIndicator.isHidden = true - loadingIndicator.setAccessibilityIdentifier("FileExplorerLoadingIndicator") + badgeLabel.translatesAutoresizingMaskIntoConstraints = false + badgeLabel.alignment = .right + badgeLabel.maximumNumberOfLines = 1 + badgeLabel.setContentHuggingPriority(.required, for: .horizontal) + badgeLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + badgeLabel.isHidden = true addSubview(iconView) addSubview(nameLabel) - addSubview(loadingIndicator) + addSubview(badgeLabel) iconWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: 16) iconHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: 16) iconToTextConstraint = nameLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 4) - loadingWidthConstraint = loadingIndicator.widthAnchor.constraint(equalToConstant: 0) - + badgeTrailingConstraint = badgeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6) NSLayoutConstraint.activate([ - iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), + iconView.leadingAnchor.constraint(equalTo: leadingAnchor), iconView.centerYAnchor.constraint(equalTo: centerYAnchor), iconWidthConstraint, iconHeightConstraint, - iconToTextConstraint, nameLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - - loadingIndicator.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), - loadingIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), - loadingWidthConstraint, - loadingIndicator.heightAnchor.constraint(equalToConstant: 12), + nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: badgeLabel.leadingAnchor, constant: -4), + badgeLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + badgeTrailingConstraint, ]) + } - nameLabelTrailingToLoadingConstraint = nameLabel.trailingAnchor.constraint( - equalTo: loadingIndicator.leadingAnchor, - constant: -2 - ) - nameLabelTrailingToContainerConstraint = nameLabel.trailingAnchor.constraint( - equalTo: trailingAnchor, - constant: -2 - ) - NSLayoutConstraint.activate([ - nameLabelTrailingToLoadingConstraint, - nameLabelTrailingToContainerConstraint - ]) - nameLabelTrailingToLoadingConstraint.isActive = false - } - - func configure(with node: FileExplorerNode, gitStatus: GitFileStatus? = nil) { - assert(Thread.isMainThread, "AppKit image updates must run on the main thread") - let style = FileExplorerStyle.current - nameLabel.stringValue = node.name - nameLabel.font = style.nameFont - iconWidthConstraint.constant = style.iconSize - iconHeightConstraint.constant = style.iconSize - iconToTextConstraint.constant = style.iconToTextSpacing - - if style == .finder { - // Native Finder icon pixels miss 3:1 in light mode; use their masks with the dynamic palette tint. - if node.isDirectory { - iconView.apply(CmuxResolvedIconRequest( - source: .image(NSWorkspace.shared.icon(for: .folder)), - size: NSSize(width: style.iconSize, height: style.iconSize), - tintColor: style.folderIconTint - )) - } else { - let pathExtension = (node.name as NSString).pathExtension - iconView.apply(CmuxResolvedIconRequest( - source: .image(NSWorkspace.shared.icon(for: UTType(filenameExtension: pathExtension) ?? .data)), - size: NSSize(width: style.iconSize, height: style.iconSize), - tintColor: style.fileIconTint - )) - } - } else { - if node.isDirectory { - iconView.apply(CmuxResolvedIconRequest( - source: .systemSymbol(name: "folder.fill", accessibilityDescription: nil), - size: NSSize(width: style.iconSize, height: style.iconSize), - tintColor: style.folderIconTint, - symbolWeight: style.iconWeight - )) - } else { - iconView.apply(CmuxResolvedIconRequest( - source: .systemSymbol(name: "doc", accessibilityDescription: nil), - size: NSSize(width: style.iconSize, height: style.iconSize), - tintColor: style.fileIconTint, - symbolWeight: style.iconWeight - )) - } + /// Applies a row's state. Writes are skipped when unchanged. + func configure( + with node: FileExplorerNode, + gitStatus: GitFileStatus? = nil, + style: FileExplorerStyle = .current, + iconCache: FileExplorerIconCache? = nil + ) { + assert(Thread.isMainThread, "AppKit updates must run on the main thread") + self.node = node + self.iconCache = iconCache + setIfChanged(nameLabel.stringValue, node.name) { nameLabel.stringValue = $0 } + let font = style.nameFont + if nameLabel.font != font { nameLabel.font = font } + if iconWidthConstraint.constant != style.iconSize { + iconWidthConstraint.constant = style.iconSize + iconHeightConstraint.constant = style.iconSize } - - if node.isLoading { - loadingWidthConstraint.constant = 12 - loadingIndicator.isHidden = false - loadingIndicator.startAnimation(nil) - nameLabelTrailingToLoadingConstraint.isActive = true - nameLabelTrailingToContainerConstraint.isActive = false - } else { - loadingWidthConstraint.constant = 0 - loadingIndicator.isHidden = true - loadingIndicator.stopAnimation(nil) - nameLabelTrailingToLoadingConstraint.isActive = false - nameLabelTrailingToContainerConstraint.isActive = true + if iconToTextConstraint.constant != style.iconToTextSpacing { + iconToTextConstraint.constant = style.iconToTextSpacing } + applyIcon(for: node, style: style) + applyLoading(node.isLoading) + let textColor: NSColor + let toolTip: String if let error = node.error { - nameLabel.textColor = .systemRed - nameLabel.toolTip = error + textColor = .systemRed + toolTip = error } else if let gitStatus { - nameLabel.textColor = style.gitColor(for: gitStatus) - nameLabel.toolTip = node.path + textColor = style.gitColor(for: gitStatus) + toolTip = node.path } else { - nameLabel.textColor = .labelColor - nameLabel.toolTip = node.path + textColor = .labelColor + toolTip = node.path + } + if nameLabel.textColor != textColor { nameLabel.textColor = textColor } + setIfChanged(nameLabel.toolTip, toolTip) { nameLabel.toolTip = $0 } + applyBadge(node: node, gitStatus: gitStatus, style: style) + setAccessibilityLabel(node.name) + } + + private func applyIcon(for node: FileExplorerNode, style: FileExplorerStyle) { + let appearanceName = effectiveAppearance.name + let nodeKey = node.isDirectory ? node.path + "/" : node.path + if let key = renderedIconKey, key.style == style, key.appearance == appearanceName, key.nodeKey == nodeKey { + return + } + renderedIconKey = (style, appearanceName, nodeKey) + let cache = iconCache ?? FileExplorerIconCache() + iconView.image = cache.icon(for: node, style: style, appearance: effectiveAppearance) + } + + private func applyLoading(_ isLoading: Bool) { + if isLoading { + let indicator = loadingIndicator ?? makeLoadingIndicator() + if indicator.isHidden { + indicator.isHidden = false + indicator.startAnimation(nil) + } + badgeTrailingConstraint.constant = -22 + } else if let indicator = loadingIndicator, !indicator.isHidden { + indicator.stopAnimation(nil) + indicator.isHidden = true + badgeTrailingConstraint.constant = -6 + } + } + + private func makeLoadingIndicator() -> NSProgressIndicator { + let indicator = NSProgressIndicator() + indicator.translatesAutoresizingMaskIntoConstraints = false + indicator.style = .spinning + indicator.controlSize = .small + indicator.isHidden = true + indicator.setAccessibilityIdentifier("FileExplorerLoadingIndicator") + addSubview(indicator) + NSLayoutConstraint.activate([ + indicator.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), + indicator.centerYAnchor.constraint(equalTo: centerYAnchor), + indicator.widthAnchor.constraint(equalToConstant: 12), + indicator.heightAnchor.constraint(equalToConstant: 12), + ]) + loadingIndicator = indicator + return indicator + } + + private func applyBadge(node: FileExplorerNode, gitStatus: GitFileStatus?, style: FileExplorerStyle) { + var text = "" + var color = NSColor.secondaryLabelColor + var toolTip: String? + if node.omittedCount > 0 { + text = "+\(node.omittedCount)" + toolTip = String( + localized: "fileExplorer.omittedEntries", + defaultValue: "This folder has more items than the remote listing returns." + ) + } else if let gitStatus, !node.isDirectory { + text = Self.badgeText(for: gitStatus) + color = style.gitColor(for: gitStatus) + } else if let gitStatus, node.isDirectory, gitStatus != .deleted { + text = "•" + color = style.gitColor(for: gitStatus) + } + let hidden = text.isEmpty + if badgeLabel.isHidden != hidden { badgeLabel.isHidden = hidden } + guard !hidden else { return } + setIfChanged(badgeLabel.stringValue, text) { badgeLabel.stringValue = $0 } + if badgeLabel.textColor != color { badgeLabel.textColor = color } + let badgeFont = NSFont.monospacedSystemFont(ofSize: max(9, style.nameFont.pointSize - 2), weight: .semibold) + if badgeLabel.font != badgeFont { badgeLabel.font = badgeFont } + badgeLabel.toolTip = toolTip + } + + private static func badgeText(for status: GitFileStatus) -> String { + switch status { + case .modified: return "M" + case .added: return "A" + case .deleted: return "D" + case .renamed: return "R" + case .untracked: return "U" } } + private func setIfChanged(_ current: Value, _ next: Value, _ apply: (Value) -> Void) { + if current != next { apply(next) } + } + + override func viewDidChangeEffectiveAppearance() { + super.viewDidChangeEffectiveAppearance() + guard let node, let key = renderedIconKey else { return } + renderedIconKey = nil + applyIcon(for: node, style: key.style) + } + + // MARK: Inline rename + + /// Turns the name into an editable field and selects the name stem, like Finder. + func beginRenaming(delegate: NSTextFieldDelegate) -> Bool { + guard let window else { return false } + nameLabel.isEditable = true + nameLabel.isSelectable = true + nameLabel.isBezeled = false + nameLabel.drawsBackground = true + nameLabel.backgroundColor = .textBackgroundColor + nameLabel.textColor = .textColor + nameLabel.delegate = delegate + nameLabel.lineBreakMode = .byClipping + guard window.makeFirstResponder(nameLabel) else { + endRenaming() + return false + } + if let editor = nameLabel.currentEditor() { + let name = nameLabel.stringValue as NSString + let ext = name.pathExtension + let stemLength = (node?.isDirectory == false && !ext.isEmpty && name.length > ext.count + 1) + ? name.length - ext.count - 1 + : name.length + editor.selectedRange = NSRange(location: 0, length: stemLength) + } + return true + } + + func endRenaming() { + nameLabel.isEditable = false + nameLabel.isSelectable = false + nameLabel.drawsBackground = false + nameLabel.delegate = nil + nameLabel.lineBreakMode = .byTruncatingMiddle + if let node { nameLabel.stringValue = node.name } + } + + var isRenaming: Bool { nameLabel.isEditable } + + // MARK: Hover prefetch + override func updateTrackingAreas() { super.updateTrackingAreas() if let existing = trackingArea { removeTrackingArea(existing) + trackingArea = nil } + guard onHover != nil else { return } let area = NSTrackingArea( rect: bounds, options: [.mouseEnteredAndExited, .activeInActiveApp], diff --git a/Sources/FileExplorerContainerView+SearchStatus.swift b/Sources/FileExplorerContainerView+SearchStatus.swift deleted file mode 100644 index 1f58ad32bbd4..000000000000 --- a/Sources/FileExplorerContainerView+SearchStatus.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -// Search state copy shared by local and Cloud results. -extension FileExplorerContainerView { - func statusText(for snapshot: FileSearchSnapshot) -> String { - switch snapshot.status { - case .idle: - return "" - case .unsupported: - return String(localized: "fileExplorer.search.unsupported", defaultValue: "Local folders only") - case .searching: - return String( - format: String(localized: "fileExplorer.search.searching", defaultValue: "%d matches, searching"), - snapshot.results.count - ) - case .noMatches: - return String(localized: "fileExplorer.search.noMatches", defaultValue: "No matches") - case .matches: - return String( - format: String(localized: "fileExplorer.search.matches", defaultValue: "%d matches"), - snapshot.results.count - ) - case .limited(let limit): - return String( - format: String(localized: "fileExplorer.search.limit", defaultValue: "First %d matches"), - limit - ) - case .failed(let message): - return String( - format: String(localized: "fileExplorer.search.failed", defaultValue: "Search failed: %@"), - message - ) - } - } - -} diff --git a/Sources/FileExplorerError.swift b/Sources/FileExplorerError.swift new file mode 100644 index 000000000000..1788d0fc1690 --- /dev/null +++ b/Sources/FileExplorerError.swift @@ -0,0 +1,24 @@ +import Foundation + +enum FileExplorerError: LocalizedError { + case providerUnavailable + case sshCommandFailed(String) + case remoteCommandFailed(String) + case previewCapacity + case remoteFileTooLarge + + var errorDescription: String? { + switch self { + case .providerUnavailable: + return String(localized: "fileExplorer.error.unavailable", defaultValue: "File explorer is not available") + case .sshCommandFailed: + return String(localized: "fileExplorer.error.sshFailed", defaultValue: "SSH command failed") + case .previewCapacity: + return String(localized: "fileExplorer.preview.capacity", defaultValue: "Close a Cloud file preview and try again.") + case .remoteFileTooLarge: + return String(localized: "fileExplorer.error.cloudPreviewTooLarge", defaultValue: "Cloud file previews are limited to 1 MB.") + case .remoteCommandFailed: + return String(localized: "fileExplorer.error.remoteFailed", defaultValue: "Remote command failed") + } + } +} diff --git a/Sources/FileExplorerHeaderView.swift b/Sources/FileExplorerHeaderView.swift index bc11e4392348..52a1adb05240 100644 --- a/Sources/FileExplorerHeaderView.swift +++ b/Sources/FileExplorerHeaderView.swift @@ -6,7 +6,15 @@ import CmuxFoundation final class FileExplorerHeaderView: NSView { private let iconView = CmuxResolvedIconImageView() private let retryButton = NSButton() + private let viewOptionsButton = NSButton() private var retry: (() -> Void)? + /// Builds the view-options menu (hidden files, sort, refresh) on demand. + var viewOptionsMenuProvider: (() -> NSMenu)? { + didSet { + let hidden = viewOptionsMenuProvider == nil + if viewOptionsButton.isHidden != hidden { viewOptionsButton.isHidden = hidden } + } + } private let pathLabel = NSTextField(labelWithString: "") private var heightConstraint: NSLayoutConstraint? private var displayPath = "" @@ -42,6 +50,17 @@ final class FileExplorerHeaderView: NSView { retryButton.toolTip = String(localized: "common.retry", defaultValue: "Retry") retryButton.setAccessibilityLabel(retryButton.toolTip) addSubview(retryButton) + viewOptionsButton.translatesAutoresizingMaskIntoConstraints = false + viewOptionsButton.image = NSImage(systemSymbolName: "ellipsis.circle", accessibilityDescription: nil) + viewOptionsButton.bezelStyle = .inline + viewOptionsButton.isBordered = false + viewOptionsButton.target = self + viewOptionsButton.action = #selector(showViewOptions) + viewOptionsButton.toolTip = String(localized: "fileExplorer.header.viewOptions", defaultValue: "View Options") + viewOptionsButton.setAccessibilityLabel(viewOptionsButton.toolTip) + viewOptionsButton.setAccessibilityIdentifier("FileExplorerViewOptionsButton") + viewOptionsButton.isHidden = true + addSubview(viewOptionsButton) let heightConstraint = heightAnchor.constraint(equalToConstant: RightSidebarChromeMetrics.secondaryBarHeight) self.heightConstraint = heightConstraint @@ -57,7 +76,10 @@ final class FileExplorerHeaderView: NSView { pathLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: RightSidebarChromeMetrics.contentIconTextSpacing), pathLabel.centerYAnchor.constraint(equalTo: centerYAnchor), pathLabel.trailingAnchor.constraint(equalTo: retryButton.leadingAnchor, constant: -8), - retryButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), + retryButton.trailingAnchor.constraint(equalTo: viewOptionsButton.leadingAnchor, constant: -4), + viewOptionsButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8), + viewOptionsButton.centerYAnchor.constraint(equalTo: centerYAnchor), + viewOptionsButton.widthAnchor.constraint(equalToConstant: 18), retryButton.centerYAnchor.constraint(equalTo: centerYAnchor), retryButton.widthAnchor.constraint(equalToConstant: 18), ]) @@ -71,9 +93,16 @@ final class FileExplorerHeaderView: NSView { @objc private func retryFiles() { retry?() } + @objc private func showViewOptions() { + guard let menu = viewOptionsMenuProvider?() else { return } + let below = viewOptionsButton.isFlipped ? viewOptionsButton.bounds.maxY + 4 : -4 + menu.popUp(positioning: nil, at: NSPoint(x: 0, y: below), in: viewOptionsButton) + } + func update(displayPath: String, retry: (() -> Void)? = nil) { self.retry = retry - retryButton.isHidden = retry == nil + let retryHidden = retry == nil + if retryButton.isHidden != retryHidden { retryButton.isHidden = retryHidden } guard self.displayPath != displayPath else { return } self.displayPath = displayPath applyHeaderState() diff --git a/Sources/FileExplorerIconCache.swift b/Sources/FileExplorerIconCache.swift new file mode 100644 index 000000000000..14b9abe0308c --- /dev/null +++ b/Sources/FileExplorerIconCache.swift @@ -0,0 +1,84 @@ +import AppKit +import CmuxAppKitSupportUI +import UniformTypeIdentifiers + +/// Renders each Files row icon once per kind, style and appearance. +/// +/// Rows share a handful of icons (folder, one per file type). Rendering the +/// tinted icon on every cell reuse cost a bitmap draw per scrolled row; the +/// cache turns scrolling a 50,000-row folder into image lookups. +@MainActor +final class FileExplorerIconCache { + private struct Key: Hashable { + let style: Int + let kind: String + let appearance: String + let size: CGFloat + } + + private let renderer = CmuxResolvedIconRenderer() + private var images: [Key: NSImage] = [:] + private var typeByExtension: [String: UTType] = [:] + private let capacity = 512 + + /// The icon for `node` in `style` under `appearance`. + func icon(for node: FileExplorerNode, style: FileExplorerStyle, appearance: NSAppearance) -> NSImage? { + let kind = kindKey(for: node, style: style) + let appearanceName = appearance.bestMatch(from: [ + .aqua, .darkAqua, .accessibilityHighContrastAqua, .accessibilityHighContrastDarkAqua, + ])?.rawValue ?? appearance.name.rawValue + let key = Key(style: style.rawValue, kind: kind, appearance: appearanceName, size: style.iconSize) + if let cached = images[key] { return cached } + let request = self.request(for: node, kind: kind, style: style) + guard let image = renderer.image(for: request, appearance: appearance) else { return nil } + if images.count >= capacity { images.removeAll(keepingCapacity: true) } + images[key] = image + return image + } + + private func kindKey(for node: FileExplorerNode, style: FileExplorerStyle) -> String { + let ext = node.entry.pathExtension + if node.isDirectory { + guard style == .finder, !ext.isEmpty, packageType(forExtension: ext) != nil else { return "folder" } + return "package:" + ext + } + guard style == .finder else { return "file" } + return ext.isEmpty ? "file" : "ext:" + ext + } + + private func packageType(forExtension ext: String) -> UTType? { + guard let type = contentType(forExtension: ext), type.conforms(to: .package) else { return nil } + return type + } + + private func contentType(forExtension ext: String) -> UTType? { + if let cached = typeByExtension[ext] { return cached } + let type = UTType(filenameExtension: ext) + if let type { typeByExtension[ext] = type } + return type + } + + private func request(for node: FileExplorerNode, kind: String, style: FileExplorerStyle) -> CmuxResolvedIconRequest { + let size = NSSize(width: style.iconSize, height: style.iconSize) + if style == .finder { + // Native Finder icon pixels miss 3:1 in light mode; use their masks with the dynamic palette tint. + let type: UTType + if node.isDirectory { + type = packageType(forExtension: node.entry.pathExtension) ?? .folder + } else { + type = contentType(forExtension: node.entry.pathExtension) ?? .data + } + return CmuxResolvedIconRequest( + source: .image(NSWorkspace.shared.icon(for: type)), + size: size, + tintColor: node.isDirectory ? style.folderIconTint : style.fileIconTint + ) + } + return CmuxResolvedIconRequest( + source: .systemSymbol(name: node.isDirectory ? "folder.fill" : "doc", accessibilityDescription: nil), + size: size, + tintColor: node.isDirectory ? style.folderIconTint : style.fileIconTint, + symbolWeight: style.iconWeight + ) + } +} diff --git a/Sources/FileExplorerKeyboardShortcuts.swift b/Sources/FileExplorerKeyboardShortcuts.swift index da2ae5971f0a..55e7745e76a2 100644 --- a/Sources/FileExplorerKeyboardShortcuts.swift +++ b/Sources/FileExplorerKeyboardShortcuts.swift @@ -56,6 +56,34 @@ extension FileExplorerNSOutlineView { fileExplorerCoordinator?.openSelectedNode(in: self) return true } + + /// Finder actions bound in Settings: Quick Look, Rename, Show Hidden + /// Files and Enclosing Folder, plus Command-Delete for Move to Trash. + func handleFinderActionShortcut(_ event: NSEvent) -> Bool { + guard let coordinator = fileExplorerCoordinator else { return false } + if event.isFileExplorerMoveToTrashKey { + endQuickSearch() + coordinator.moveToTrash(coordinator.selectedNodes(in: self)) + return true + } + guard let action = event.fileExplorerFinderAction(in: fileExplorerPanelPlacement) else { return false } + endQuickSearch() + switch action { + case .fileExplorerQuickLook: + toggleQuickLook() + case .fileExplorerRenameSelection: + if let node = coordinator.selectedNodes(in: self).first { + coordinator.beginRenaming(node) + } + case .fileExplorerToggleHiddenFiles: + coordinator.toggleHiddenFiles(nil) + case .fileExplorerSelectParent: + coordinator.selectParentOfSelection(in: self) + default: + return false + } + return true + } } extension FileExplorerSearchResultsTableView { @@ -89,6 +117,24 @@ extension NSEvent { KeyboardShortcutSettings.effectiveWhenClause(for: action).evaluate(context) } } + + /// The Settings-backed Finder action this key event triggers in the tree. + func fileExplorerFinderAction(in placement: FileExplorerPanelPlacement) -> KeyboardShortcutSettings.Action? { + guard type == .keyDown else { return nil } + let context = placement.openSelectionShortcutContext(for: self) + return KeyboardShortcutSettings.Action.fileExplorerFinderActions.first { action in + KeyboardShortcutSettings.shortcut(for: action).matches(event: self) && + KeyboardShortcutSettings.effectiveWhenClause(for: action).evaluate(context) + } + } + + /// Command-Delete, Finder's Move to Trash. Delete has no representation + /// in the shortcut recorder, so this standard editing key stays fixed. + var isFileExplorerMoveToTrashKey: Bool { + guard type == .keyDown, keyCode == 51 || keyCode == 117 else { return false } + let flags = modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting([.numericPad, .function]) + return flags == .command + } } @MainActor @@ -107,8 +153,13 @@ private extension FileExplorerPanelPlacement { } } -private extension KeyboardShortcutSettings.Action { +extension KeyboardShortcutSettings.Action { static var fileExplorerOpenSelectionActions: [Self] { [.fileExplorerOpenSelection, .fileExplorerOpenSelectionFinderAlias] } + + /// Tree actions matched inside the Files outline, in match priority order. + static var fileExplorerFinderActions: [Self] { + [.fileExplorerQuickLook, .fileExplorerRenameSelection, .fileExplorerToggleHiddenFiles, .fileExplorerSelectParent] + } } diff --git a/Sources/FileExplorerNSOutlineView.swift b/Sources/FileExplorerNSOutlineView.swift index 1a341176b037..9c99523dfab8 100644 --- a/Sources/FileExplorerNSOutlineView.swift +++ b/Sources/FileExplorerNSOutlineView.swift @@ -1,6 +1,9 @@ import AppKit +import Quartz -/// NSOutlineView subclass that disables expand/collapse animations and adds leading margin. +/// NSOutlineView subclass for the Files tree: no expand/collapse animation, +/// leading margin, Finder keyboard behavior (type-select, Option-arrow +/// recursive disclosure, Space for Quick Look) and Edit menu copy/paste. final class FileExplorerNSOutlineView: NSOutlineView { /// Leading margin applied to disclosure triangles and content. static let leadingMargin: CGFloat = 8 @@ -33,6 +36,12 @@ final class FileExplorerNSOutlineView: NSOutlineView { var onContextMenuDidClose: (() -> Void)? private var quickSearchActive = false private var quickSearchQuery = "" + /// Finder type-select buffer; it resets after a pause between keys. + private var typeSelectBuffer = "" + private var typeSelectLastTimestamp: TimeInterval = 0 + private static let typeSelectResetInterval: TimeInterval = 1.0 + /// True while AppKit collapses an item together with its descendants. + private(set) var isCollapsingChildren = false override func mouseDown(with event: NSEvent) { onNativeDragPointerBoundary?() @@ -40,6 +49,9 @@ final class FileExplorerNSOutlineView: NSOutlineView { } override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + if !isContextMenuOpen { + fileExplorerCoordinator?.contextMenuDidOpen() + } isContextMenuOpen = true super.willOpenMenu(menu, with: event) } @@ -63,7 +75,11 @@ final class FileExplorerNSOutlineView: NSOutlineView { return } - if handleOpenSelectionShortcut(event) { + if handleTypeSelectContinuation(event) { + return + } + + if handleOpenSelectionShortcut(event) || handleFinderActionShortcut(event) { return } @@ -71,6 +87,10 @@ final class FileExplorerNSOutlineView: NSOutlineView { return } + if handleRecursiveDisclosure(event) || handleExtendSelection(event) { + return + } + if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { endQuickSearch() fileExplorerCoordinator?.moveSelection(in: self, by: delta) @@ -88,6 +108,10 @@ final class FileExplorerNSOutlineView: NSOutlineView { return } + if handleTypeSelectStart(event) { + return + } + if RightSidebarKeyboardNavigation.isPlainPrintableText(event) { return } @@ -100,12 +124,15 @@ final class FileExplorerNSOutlineView: NSOutlineView { handleQuickSearchKey(event) { return true } - if handleOpenSelectionShortcut(event) { + if handleOpenSelectionShortcut(event) || handleFinderActionShortcut(event) { return true } if quickSearchActive, handleQuickSearchKey(event) { return true } + if handleRecursiveDisclosure(event) { + return true + } if let delta = RightSidebarKeyboardNavigation.moveDelta(for: event) { endQuickSearch() fileExplorerCoordinator?.moveSelection(in: self, by: delta) @@ -119,6 +146,122 @@ final class FileExplorerNSOutlineView: NSOutlineView { return super.performKeyEquivalent(with: event) } + // MARK: - Finder keyboard behavior + + /// Option-Right and Option-Left expand or collapse the selection recursively. + private func handleRecursiveDisclosure(_ event: NSEvent) -> Bool { + guard event.type == .keyDown else { return false } + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + .subtracting([.numericPad, .function]) + guard flags == .option, event.keyCode == 123 || event.keyCode == 124 else { return false } + endQuickSearch() + fileExplorerCoordinator?.performRecursiveDisclosure(expand: event.keyCode == 124, in: self) + return true + } + + /// Shift-Up and Shift-Down extend the selection like any AppKit list. + private func handleExtendSelection(_ event: NSEvent) -> Bool { + let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) + .subtracting([.numericPad, .function]) + guard flags == .shift, event.keyCode == 125 || event.keyCode == 126 else { return false } + endQuickSearch() + super.keyDown(with: event) + return true + } + + /// Continues a type-select burst: once it started, every printable key + /// (including Space and the Vim letters) extends the name being typed. + private func handleTypeSelectContinuation(_ event: NSEvent) -> Bool { + guard !typeSelectBuffer.isEmpty, !quickSearchActive, + RightSidebarKeyboardNavigation.isPlainPrintableText(event), + event.timestamp - typeSelectLastTimestamp < Self.typeSelectResetInterval, + let text = event.characters, !text.isEmpty else { + if event.timestamp - typeSelectLastTimestamp >= Self.typeSelectResetInterval { + typeSelectBuffer = "" + } + return false + } + typeSelectBuffer += text + typeSelectLastTimestamp = event.timestamp + fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: typeSelectBuffer) + return true + } + + /// Starts type-select on a printable key that is not a navigation key. + /// `h`, `j`, `k`, `l` and `/` keep their Vim and quick-search meaning. + private func handleTypeSelectStart(_ event: NSEvent) -> Bool { + guard !quickSearchActive, RightSidebarKeyboardNavigation.isPlainPrintableText(event), + let text = event.characters, !text.isEmpty, text != " " else { return false } + typeSelectBuffer = text + typeSelectLastTimestamp = event.timestamp + fileExplorerCoordinator?.selectBestQuickSearchMatch(in: self, query: text) + return true + } + + // MARK: - Edit menu + + @objc func copy(_ sender: Any?) { + if fileExplorerCoordinator?.copySelection(in: self) != true { NSSound.beep() } + } + + @objc func paste(_ sender: Any?) { + if fileExplorerCoordinator?.pasteFiles(in: self) != true { NSSound.beep() } + } + + override func validateUserInterfaceItem(_ item: any NSValidatedUserInterfaceItem) -> Bool { + switch item.action { + case #selector(copy(_:)): + return numberOfSelectedRows > 0 + case #selector(paste(_:)): + return fileExplorerCoordinator?.store.supportsFileOperations == true && + NSPasteboard.general.canReadObject(forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) + default: + return super.validateUserInterfaceItem(item) + } + } + + // MARK: - Quick Look + + func toggleQuickLook() { + guard let panel = QLPreviewPanel.shared() else { return } + if QLPreviewPanel.sharedPreviewPanelExists(), panel.isVisible { + panel.orderOut(nil) + } else { + guard !quickLookURLs.isEmpty else { + NSSound.beep() + return + } + panel.makeKeyAndOrderFront(nil) + } + } + + /// Keeps an open Quick Look panel on the current selection. + func selectionDidChangeForQuickLook() { + guard QLPreviewPanel.sharedPreviewPanelExists(), let panel = QLPreviewPanel.shared(), + panel.isVisible, panel.dataSource === self else { return } + panel.reloadData() + } + + private var quickLookURLs: [URL] { + guard fileExplorerCoordinator?.store.provider is LocalFileExplorerProvider else { return [] } + return selectedRowIndexes.compactMap { item(atRow: $0) as? FileExplorerNode } + .map { URL(fileURLWithPath: $0.path) } + } + + override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool { + !quickLookURLs.isEmpty + } + + override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) { + panel.dataSource = self + panel.delegate = self + } + + override func endPreviewPanelControl(_ panel: QLPreviewPanel!) { + panel.dataSource = nil + panel.delegate = nil + } + override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() if result { @@ -137,13 +280,22 @@ final class FileExplorerNSOutlineView: NSOutlineView { } override func expandItem(_ item: Any?, expandChildren: Bool) { + // Option-clicking a disclosure triangle asks AppKit to expand every + // descendant, but lazily listed folders have none yet. The store + // expands them as their listings arrive, within a folder budget. + if expandChildren, let node = item as? FileExplorerNode { + fileExplorerCoordinator?.store.expandRecursively(node: node) + } NSAnimationContext.beginGrouping() NSAnimationContext.current.duration = 0 - super.expandItem(item, expandChildren: expandChildren) + super.expandItem(item, expandChildren: false) NSAnimationContext.endGrouping() } override func collapseItem(_ item: Any?, collapseChildren: Bool) { + let wasCollapsingChildren = isCollapsingChildren + isCollapsingChildren = collapseChildren + defer { isCollapsingChildren = wasCollapsingChildren } NSAnimationContext.beginGrouping() NSAnimationContext.current.duration = 0 super.collapseItem(item, collapseChildren: collapseChildren) @@ -201,6 +353,13 @@ final class FileExplorerNSOutlineView: NSOutlineView { endQuickSearch() return true } + if event.keyCode == 125 || event.keyCode == 126 { + // Down/Up step through matches without leaving quick search. + fileExplorerCoordinator?.selectNextQuickSearchMatch( + in: self, query: quickSearchQuery, forward: event.keyCode == 125 + ) + return true + } if event.keyCode == 51 { if !quickSearchQuery.isEmpty { quickSearchQuery.removeLast() @@ -221,3 +380,23 @@ final class FileExplorerNSOutlineView: NSOutlineView { return true } } + +extension FileExplorerNSOutlineView: QLPreviewPanelDataSource, QLPreviewPanelDelegate { + func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { + quickLookURLs.count + } + + func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> (any QLPreviewItem)! { + let urls = quickLookURLs + guard index >= 0, index < urls.count else { return nil } + return urls[index] as NSURL + } + + func previewPanel(_ panel: QLPreviewPanel!, handle event: NSEvent!) -> Bool { + // Arrow keys in the panel move the tree selection, like Finder. + guard let event, event.type == .keyDown, + event.keyCode == 125 || event.keyCode == 126 else { return false } + fileExplorerCoordinator?.moveSelection(in: self, by: event.keyCode == 125 ? 1 : -1) + return true + } +} diff --git a/Sources/FileExplorerNode.swift b/Sources/FileExplorerNode.swift new file mode 100644 index 000000000000..c5ded669d42f --- /dev/null +++ b/Sources/FileExplorerNode.swift @@ -0,0 +1,67 @@ +import AppKit +import CmuxFileTree + +/// One row in the Files outline. +/// +/// Nodes are reference objects with a stable identity per path: a refresh +/// reuses the same node for a surviving path, so `NSOutlineView` keeps its +/// selection, expansion and cell across incremental updates. `children` is +/// `nil` until the directory has been listed. +@MainActor +final class FileExplorerNode: NSObject { + let path: String + private(set) var entry: FileTreeEntry + weak var parent: FileExplorerNode? + /// Children in display order; `nil` while the directory is unlisted. + var children: [FileExplorerNode]? + /// Whether a listing for this directory is in flight. + var isLoading = false + /// The last listing error, shown on the row until a listing succeeds. + var error: String? + /// Entries the provider left out of this directory's listing. + var omittedCount = 0 + /// A collapsed directory whose contents changed since it was listed. It + /// re-lists on the next expansion instead of on every change event. + var isStale = false + var resourceContextID: UUID? + + var id: String { path } + var name: String { entry.name } + var isDirectory: Bool { entry.isDirectory } + var isExpandable: Bool { entry.isDirectory } + var isSymbolicLink: Bool { + entry.kind == .symbolicLink || entry.kind == .symbolicLinkToDirectory + } + + init(entry: FileTreeEntry, parent: FileExplorerNode?, resourceContextID: UUID?) { + self.path = entry.path + self.entry = entry + self.parent = parent + self.resourceContextID = resourceContextID + } + + /// Creates a detached node; used by tests and drag fixtures. + convenience init(name: String, path: String, isDirectory: Bool) { + self.init( + entry: FileTreeEntry(name: name, path: path, kind: isDirectory ? .directory : .file), + parent: nil, + resourceContextID: nil + ) + } + + /// Replaces the entry for a surviving path whose metadata changed. + func update(entry: FileTreeEntry) { + guard entry.path == path else { return } + self.entry = entry + } + + /// Whether this node's path is `other` or lies below it. + func isDescendant(of other: FileExplorerNode) -> Bool { + var cursor = parent + while let node = cursor { + if node === other { return true } + cursor = node.parent + } + return false + } +} diff --git a/Sources/FileExplorerOutlineCoordinator+Actions.swift b/Sources/FileExplorerOutlineCoordinator+Actions.swift new file mode 100644 index 000000000000..81dd2a18095c --- /dev/null +++ b/Sources/FileExplorerOutlineCoordinator+Actions.swift @@ -0,0 +1,466 @@ +import AppKit +import CmuxFileTree + +/// Context menu, Finder-style file actions, inline rename and drops. +extension FileExplorerPanelView.Coordinator: NSTextFieldDelegate { + // MARK: - Context menu + + func menuNeedsUpdate(_ menu: NSMenu) { + menu.removeAllItems() + guard let outlineView else { return } + let clickedRow = outlineView.clickedRow + let clicked = clickedRow >= 0 ? outlineView.item(atRow: clickedRow) as? FileExplorerNode : nil + let targets = clicked.map { contextMenuNodes(clicked: $0) } ?? [] + let isLocal = store.provider is LocalFileExplorerProvider + + if let node = clicked { + addOpenItems(for: node, isLocal: isLocal, to: menu) + if isLocal { + menu.addItem(item(FileExternalOpenText.revealInFinder, #selector(contextMenuRevealInFinder(_:)), node)) + } + menu.addItem(.separator()) + } + + if store.supportsFileOperations { + let directory = targetDirectory(for: clicked) + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.newFile", defaultValue: "New File"), + #selector(contextMenuNewFile(_:)), directory as NSString + )) + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.newFolder", defaultValue: "New Folder"), + #selector(contextMenuNewFolder(_:)), directory as NSString + )) + if let node = clicked { + if targets.count == 1 { + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.rename", defaultValue: "Rename"), + #selector(contextMenuRename(_:)), node + )) + } + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.moveToTrash", defaultValue: "Move to Trash"), + #selector(contextMenuMoveToTrash(_:)), node + )) + } + menu.addItem(.separator()) + } + + if let node = clicked { + menu.addFileExplorerInsertPathItems( + target: self, + representedObject: node, + insertAction: #selector(contextMenuInsertPath(_:)), + insertRelativeAction: #selector(contextMenuInsertRelativePath(_:)) + ) + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.copyPath", defaultValue: "Copy Path"), + #selector(contextMenuCopyPath(_:)), node + )) + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.copyRelativePath", defaultValue: "Copy Relative Path"), + #selector(contextMenuCopyRelativePath(_:)), node + )) + menu.addItem(.separator()) + } + + addViewOptionItems(to: menu) + } + + /// Show Hidden Files, Sort By and Refresh; shared by the context menu and + /// the header's view-options button. + func addViewOptionItems(to menu: NSMenu) { + let hidden = item( + String(localized: "fileExplorer.contextMenu.showHiddenFiles", defaultValue: "Show Hidden Files"), + #selector(toggleHiddenFiles(_:)), nil + ) + hidden.state = store.showHiddenFiles ? .on : .off + menu.addItem(hidden) + + let sortMenu = NSMenu(title: String(localized: "fileExplorer.contextMenu.sortBy", defaultValue: "Sort By")) + let keys: [(FileTreeSortOrder.Key, String)] = [ + (.name, String(localized: "fileExplorer.sort.name", defaultValue: "Name")), + (.kind, String(localized: "fileExplorer.sort.kind", defaultValue: "Kind")), + (.dateModified, String(localized: "fileExplorer.sort.dateModified", defaultValue: "Date Modified")), + (.size, String(localized: "fileExplorer.sort.size", defaultValue: "Size")), + ] + for (key, title) in keys { + let sortItem = item(title, #selector(selectSortKey(_:)), key.rawValue as NSString) + sortItem.state = store.sortOrder.key == key ? .on : .off + sortMenu.addItem(sortItem) + } + sortMenu.addItem(.separator()) + let ascending = item( + String(localized: "fileExplorer.sort.ascending", defaultValue: "Ascending"), + #selector(selectSortDirection(_:)), NSNumber(value: true) + ) + ascending.state = store.sortOrder.ascending ? .on : .off + sortMenu.addItem(ascending) + let descending = item( + String(localized: "fileExplorer.sort.descending", defaultValue: "Descending"), + #selector(selectSortDirection(_:)), NSNumber(value: false) + ) + descending.state = store.sortOrder.ascending ? .off : .on + sortMenu.addItem(descending) + sortMenu.addItem(.separator()) + let foldersFirst = item( + String(localized: "fileExplorer.sort.foldersFirst", defaultValue: "Keep Folders on Top"), + #selector(toggleFoldersFirst(_:)), nil + ) + foldersFirst.state = store.sortOrder.foldersFirst ? .on : .off + sortMenu.addItem(foldersFirst) + let sortItem = NSMenuItem(title: sortMenu.title, action: nil, keyEquivalent: "") + sortItem.submenu = sortMenu + menu.addItem(sortItem) + + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.refresh", defaultValue: "Refresh"), + #selector(refreshTree(_:)), nil + )) + } + + private func addOpenItems(for node: FileExplorerNode, isLocal: Bool, to menu: NSMenu) { + if node.isDirectory { + menu.addItem(item(String(localized: "fileExplorer.contextMenu.open", defaultValue: "Open"), + #selector(contextMenuOpen(_:)), node)) + return + } + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.openInCmux", defaultValue: "Open in cmux"), + #selector(contextMenuOpenInCmux(_:)), node + )) + if isLocal { + FileExplorerExternalOpenMenuItems( + fileURL: URL(fileURLWithPath: node.path), + target: self, + action: #selector(contextMenuOpenExternally(_:)) + ).add(to: menu) + menu.addItem(item( + String(localized: "fileExplorer.contextMenu.quickLook", defaultValue: "Quick Look"), + #selector(contextMenuQuickLook(_:)), node + )) + } + } + + private func item(_ title: String, _ action: Selector, _ represented: Any?) -> NSMenuItem { + let menuItem = NSMenuItem(title: title, action: action, keyEquivalent: "") + menuItem.target = self + menuItem.representedObject = represented + return menuItem + } + + func contextMenuNodes(clicked node: FileExplorerNode) -> [FileExplorerNode] { + guard let outlineView else { return [node] } + let clickedRow = outlineView.clickedRow + let selectedRows = outlineView.selectedRowIndexes + guard clickedRow >= 0, selectedRows.contains(clickedRow) else { + return [node] + } + let nodes = selectedRows.compactMap { row -> FileExplorerNode? in + guard row >= 0, row < outlineView.numberOfRows else { return nil } + return outlineView.item(atRow: row) as? FileExplorerNode + } + return nodes.isEmpty ? [node] : nodes + } + + /// New items go into a clicked folder, a clicked file's folder, or the root. + func targetDirectory(for node: FileExplorerNode?) -> String { + guard let node else { return store.rootPath } + if node.isDirectory { return node.path } + return node.parent?.path ?? store.rootPath + } + + func contextMenuDidOpen() { + store.freezeTreeUpdates() + } + + /// Updates wait for the menu highlight to finish tearing down (#12914). + func contextMenuDidClose() { + Task { @MainActor [weak self] in + self?.store.thawTreeUpdates() + } + } + + // MARK: - Menu actions + + @objc func contextMenuOpen(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode, let outlineView else { return } + let row = outlineView.row(forItem: node) + if row >= 0 { openNode(in: outlineView, at: row) } + } + + @objc func contextMenuOpenInCmux(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode else { return } + onOpenFilePreview(node.path) + } + + @objc func contextMenuOpenExternally(_ sender: NSMenuItem) { + guard let request = sender.representedObject as? FileExplorerExternalOpenRequest else { return } + FileExternalOpenAction.open(fileURL: request.fileURL, applicationURL: request.applicationURL) + } + + @objc func contextMenuRevealInFinder(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode else { return } + let urls = contextMenuNodes(clicked: node).map { URL(fileURLWithPath: $0.path) } + NSWorkspace.shared.activateFileViewerSelecting(urls) + } + + @objc func contextMenuQuickLook(_ sender: NSMenuItem) { + (outlineView as? FileExplorerNSOutlineView)?.toggleQuickLook() + } + + @objc func contextMenuCopyPath(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode else { return } + let paths = contextMenuNodes(clicked: node).map(\.path) + GhosttyApp.terminalPasteboard.writeString(paths.joined(separator: "\n"), to: .general) + } + + @objc func contextMenuCopyRelativePath(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode else { return } + let paths = contextMenuNodes(clicked: node).map { + FileExplorerTerminalPathInsertion.relativePath(for: $0.path, rootPath: store.rootPath) + } + GhosttyApp.terminalPasteboard.writeString(paths.joined(separator: "\n"), to: .general) + } + + @objc func contextMenuNewFile(_ sender: NSMenuItem) { + guard let directory = sender.representedObject as? String else { return } + createItem(in: directory, isFolder: false) + } + + @objc func contextMenuNewFolder(_ sender: NSMenuItem) { + guard let directory = sender.representedObject as? String else { return } + createItem(in: directory, isFolder: true) + } + + @objc func contextMenuRename(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode else { return } + beginRenaming(node) + } + + @objc func contextMenuMoveToTrash(_ sender: NSMenuItem) { + guard let node = sender.representedObject as? FileExplorerNode else { return } + moveToTrash(contextMenuNodes(clicked: node)) + } + + @objc func toggleHiddenFiles(_ sender: Any?) { + state.showHiddenFiles.toggle() + store.showHiddenFiles = state.showHiddenFiles + } + + @objc func selectSortKey(_ sender: NSMenuItem) { + guard let raw = sender.representedObject as? String, let key = FileTreeSortOrder.Key(rawValue: raw) else { return } + var order = store.sortOrder + order.key = key + applySortOrder(order) + } + + @objc func selectSortDirection(_ sender: NSMenuItem) { + guard let ascending = (sender.representedObject as? NSNumber)?.boolValue else { return } + var order = store.sortOrder + order.ascending = ascending + applySortOrder(order) + } + + @objc func toggleFoldersFirst(_ sender: NSMenuItem) { + var order = store.sortOrder + order.foldersFirst.toggle() + applySortOrder(order) + } + + private func applySortOrder(_ order: FileTreeSortOrder) { + state.sortOrder = order + store.sortOrder = order + } + + @objc func refreshTree(_ sender: Any?) { + store.refreshVisibleDirectories() + store.refreshGitStatus() + } + + // MARK: - File operations + + func createItem(in directory: String, isFolder: Bool) { + Task { @MainActor [weak self] in + guard let self else { return } + do { + try await self.store.createItem(inDirectory: directory, isFolder: isFolder) + } catch { + self.present(error) + } + } + } + + func moveToTrash(_ nodes: [FileExplorerNode]) { + guard store.supportsFileOperations, !nodes.isEmpty else { return } + // Trashing a folder and something inside it would fail for the child. + let paths = nodes.map(\.path).filter { path in + !nodes.contains { $0.path != path && FileExplorerStore.path(path, isContainedIn: $0.path) } + } + Task { @MainActor [weak self] in + guard let self else { return } + do { + try await self.store.moveToTrash(paths: paths) + } catch { + self.present(error) + } + } + } + + func present(_ error: Error) { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = String(localized: "fileExplorer.operation.failedTitle", defaultValue: "The operation couldn’t be completed") + alert.informativeText = error.localizedDescription + alert.addButton(withTitle: String(localized: "fileExplorer.preview.ok", defaultValue: "OK")) + _ = alert.runCmuxModal(presentingWindow: outlineView?.window) + } + + // MARK: - Inline rename + + func beginRenaming(_ node: FileExplorerNode) { + guard store.supportsFileOperations, let outlineView else { return } + let row = outlineView.row(forItem: node) + guard row >= 0 else { return } + cancelRenaming() + outlineView.scrollRowToVisible(row) + guard let cell = outlineView.view(atColumn: 0, row: row, makeIfNecessary: true) as? FileExplorerCellView else { return } + renamingCell = cell + renamingPath = node.path + if !cell.beginRenaming(delegate: self) { + renamingCell = nil + renamingPath = nil + } + } + + func cancelRenaming() { + guard let cell = renamingCell else { + renamingPath = nil + return + } + renamingCell = nil + renamingPath = nil + cell.endRenaming() + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + guard control === renamingCell?.textField else { return false } + if commandSelector == #selector(NSResponder.cancelOperation(_:)) { + cancelRenaming() + outlineView.map { _ = $0.window?.makeFirstResponder($0) } + return true + } + return false + } + + func controlTextDidEndEditing(_ notification: Notification) { + guard let field = notification.object as? NSTextField, field === renamingCell?.textField, + let path = renamingPath else { return } + let newName = field.stringValue + let cell = renamingCell + renamingCell = nil + renamingPath = nil + cell?.endRenaming() + if let outlineView { _ = outlineView.window?.makeFirstResponder(outlineView) } + guard newName != (path as NSString).lastPathComponent else { return } + Task { @MainActor [weak self] in + guard let self else { return } + do { + try await self.store.renameItem(atPath: path, to: newName) + } catch { + self.present(error) + } + } + } + + // MARK: - Pasteboard (Edit > Copy / Paste) + + func copySelection(in outlineView: NSOutlineView) -> Bool { + let nodes = selectedNodes(in: outlineView) + guard !nodes.isEmpty else { return false } + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + if store.provider is LocalFileExplorerProvider { + pasteboard.writeObjects(nodes.map { URL(fileURLWithPath: $0.path) as NSURL }) + } + pasteboard.setString(nodes.map(\.path).joined(separator: "\n"), forType: .string) + return true + } + + func pasteFiles(in outlineView: NSOutlineView) -> Bool { + guard store.supportsFileOperations, + let urls = NSPasteboard.general.readObjects(forClasses: [NSURL.self], + options: [.urlReadingFileURLsOnly: true]) as? [URL], !urls.isEmpty else { return false } + let anchor = store.selectedPath.flatMap { store.nodesByPath[$0] } + let directory = targetDirectory(for: anchor) + importItems(urls, into: directory, move: false) + return true + } + + func importItems(_ urls: [URL], into directory: String, move: Bool) { + Task { @MainActor [weak self] in + guard let self else { return } + do { + try await self.store.importItems(urls, into: directory, move: move) + self.fileExplorerTreeDidChangeSelection(self.store, scrollToAnchor: true) + } catch { + self.present(error) + } + } + } + + // MARK: - Drop (drag in) + + func outlineView( + _ outlineView: NSOutlineView, + validateDrop info: any NSDraggingInfo, + proposedItem item: Any?, + proposedChildIndex index: Int + ) -> NSDragOperation { + guard store.supportsFileOperations, + let urls = info.draggingPasteboard.readObjects(forClasses: [NSURL.self], + options: [.urlReadingFileURLsOnly: true]) as? [URL], !urls.isEmpty else { return [] } + var target = item as? FileExplorerNode + if let node = target, !node.isDirectory { + target = node.parent + } + // Always drop onto a folder row (or the root), never between rows. + outlineView.setDropItem(target, dropChildIndex: NSOutlineViewDropOnItemIndex) + let directory = target?.path ?? store.rootPath + if urls.contains(where: { FileExplorerStore.path(directory, isContainedIn: $0.path) }) { return [] } + return dropOperation(for: info, urls: urls, directory: directory) + } + + func outlineView( + _ outlineView: NSOutlineView, + acceptDrop info: any NSDraggingInfo, + item: Any?, + childIndex index: Int + ) -> Bool { + guard store.supportsFileOperations, + let urls = info.draggingPasteboard.readObjects(forClasses: [NSURL.self], + options: [.urlReadingFileURLsOnly: true]) as? [URL], !urls.isEmpty else { return false } + let directory = (item as? FileExplorerNode).map { $0.isDirectory ? $0.path : targetDirectory(for: $0) } + ?? store.rootPath + let operation = dropOperation(for: info, urls: urls, directory: directory) + guard !operation.isEmpty else { return false } + importItems(urls, into: directory, move: operation == .move) + return true + } + + /// Finder semantics: Option copies, otherwise same-volume drops move and + /// cross-volume drops copy. + private func dropOperation(for info: any NSDraggingInfo, urls: [URL], directory: String) -> NSDragOperation { + let mask = info.draggingSourceOperationMask + if NSEvent.modifierFlags.contains(.option), mask.contains(.copy) { return .copy } + let destinationVolume = Self.volumeIdentifier(for: URL(fileURLWithPath: directory)) + let sameVolume = destinationVolume != nil && urls.allSatisfy { Self.volumeIdentifier(for: $0) == destinationVolume } + if sameVolume, mask.contains(.move) || mask.contains(.generic) { return .move } + if mask.contains(.copy) { return .copy } + return mask.contains(.generic) ? .copy : [] + } + + private static func volumeIdentifier(for url: URL) -> NSObject? { + (try? url.resourceValues(forKeys: [.volumeIdentifierKey]))?.volumeIdentifier as? NSObject + } +} diff --git a/Sources/FileExplorerOutlineCoordinator+NativeDrag.swift b/Sources/FileExplorerOutlineCoordinator+NativeDrag.swift new file mode 100644 index 000000000000..ba53276fe5c8 --- /dev/null +++ b/Sources/FileExplorerOutlineCoordinator+NativeDrag.swift @@ -0,0 +1,267 @@ +import AppKit + +/// Native drag sources for the Files outline. +/// +/// File rows drag through ``FilePreviewDragPasteboardWriter`` so cmux panes +/// can open them as previews; that path carries the native-session ownership +/// fencing below. Folders drag as file URLs and remote rows as path text. +extension FileExplorerPanelView.Coordinator { + /// Applies the shared native-generation fence used by both file + /// preview drag sources. A distinct `willBeginAt` session is an + /// authoritative boundary and replaces the prior owner. + func supersedeNativeDragIfNeeded( + previousSession: NSDraggingSession?, + newSession: NSDraggingSession, + finishPrevious: () -> Void, + clearPrevious: () -> Void + ) { + guard let previousSession, previousSession !== newSession else { return } + // A distinct `willBeginAt` callback is itself an AppKit native + // boundary. Sequence numbers are useful for terminal fencing but + // cannot reject this promotion because the OS may reuse them. + finishPrevious() + clearPrevious() + } + + func trackNativeDrag( + sourceView: NSView, + session: NSDraggingSession, + writer: FilePreviewDragPasteboardWriter?, + ownerships: [FilePreviewNativeDragOwnership] + ) { + activeNativeDragSourceView = sourceView + activeNativeDragWriter = writer + activeNativeDragSession = session + activeNativeDragOwnerships = ownerships + } + + func clearTrackedSourceState() { + if let outlineView = activeNativeDragSourceView as? FileExplorerNSOutlineView { + outlineView.activeNativeDragDelegateMarker = nil + outlineView.activeNativeDragWriter?.releaseSourceGraph() + outlineView.activeNativeDragWriter = nil + outlineView.activeNativeDragOwnerships = [] + outlineView.activeNativeDragSession = nil + } else if let searchResultsView = activeNativeDragSourceView as? FileExplorerSearchResultsTableView { + searchResultsView.activeNativeDragDelegateMarker = nil + searchResultsView.activeNativeDragWriter?.releaseSourceGraph() + searchResultsView.activeNativeDragWriter = nil + searchResultsView.activeNativeDragOwnerships = [] + searchResultsView.activeNativeDragSession = nil + } + } + + /// Reclaims the promoted source even when its original view is no + /// longer the coordinator's current representable. + @discardableResult + func reclaimTrackedNativeDrag() -> Bool { + guard let session = activeNativeDragSession else { return false } + if activeNativeDragOwnerships.isEmpty { + FilePreviewDragPasteboardWriter.discardRegisteredDrag(from: session) + } else { + for ownership in activeNativeDragOwnerships { + ownership.finish(from: session.draggingPasteboard) + } + } + let writer = activeNativeDragWriter + clearTrackedSourceState() + writer?.releaseSourceGraph() + activeNativeDragSourceView = nil + activeNativeDragWriter = nil + activeNativeDragSession = nil + activeNativeDragOwnerships = [] + return true + } + + func isTrackingNativeDrag(_ session: NSDraggingSession) -> Bool { + activeNativeDragSession === session + } + + func forgetTrackedNativeDrag(matching session: NSDraggingSession) { + guard activeNativeDragSession === session else { return } + activeNativeDragSourceView = nil + activeNativeDragWriter = nil + activeNativeDragSession = nil + activeNativeDragOwnerships = [] + } + + func previewWriterDidDeallocate(tokenID: UUID) { + guard let outlineView = outlineView as? FileExplorerNSOutlineView, + outlineView.pendingNativeDragTokenID == tokenID else { return } + outlineView.pendingNativeDragWriter = nil + outlineView.pendingNativeDragTokenID = nil + } + + // MARK: - Drag-to-Preview + + func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> (any NSPasteboardWriting)? { + guard let node = item as? FileExplorerNode else { return nil } + guard store.provider is LocalFileExplorerProvider else { + // Remote rows drag as their remote path text, e.g. into a terminal + // attached to the same host. + return node.path as NSString + } + if node.isDirectory { + // Folders drag as plain file URLs: Finder, terminals and other + // apps accept them; cmux file previews only take files. + return URL(fileURLWithPath: node.path, isDirectory: true) as NSURL + } + let writer = FilePreviewDragPasteboardWriter( + filePath: node.path, + displayTitle: node.name, + nativeSourceView: outlineView, + // Retain the exact container/delegate graph through a + // representable rebuild; the coordinator's container edge is + // intentionally weak. + nativeSourceOwner: containerView ?? outlineView, + provisionalToken: pendingPreviewDrag.makeToken() + ) + if let outlineView = outlineView as? FileExplorerNSOutlineView { + outlineView.pendingNativeDragWriter = writer + pendingPreviewDrag.register(writer) + outlineView.pendingNativeDragTokenID = writer.provisionalToken?.id + } + return writer + } + + func outlineView( + _ outlineView: NSOutlineView, + draggingSession session: NSDraggingSession, + willBeginAt screenPoint: NSPoint, + forItems draggedItems: [Any] + ) { + _ = screenPoint + _ = draggedItems + if let outlineView = outlineView as? FileExplorerNSOutlineView { + if outlineView.activeNativeDragSession === session { + return + } + if activeNativeDragSession === session { + return + } + // A distinct begin is a native boundary, even when the prior + // source belonged to a rebuilt outline view. + _ = reclaimTrackedNativeDrag() + let fallbackWriter = outlineView.pendingNativeDragWriter + var promotedWriters = pendingPreviewDrag.writers(for: outlineView) + if let fallbackWriter, + !promotedWriters.contains(where: { $0 === fallbackWriter }) { + promotedWriters.append(fallbackWriter) + } + pendingPreviewDrag.finishPending(preserving: promotedWriters) + supersedeNativeDragIfNeeded( + previousSession: outlineView.activeNativeDragSession, + newSession: session, + finishPrevious: { + let pasteboard = outlineView.activeNativeDragSession?.draggingPasteboard + ?? session.draggingPasteboard + for ownership in outlineView.activeNativeDragOwnerships { + ownership.finish(from: pasteboard) + } + }, + clearPrevious: { + outlineView.activeNativeDragWriter?.releaseSourceGraph() + outlineView.activeNativeDragWriter = nil + outlineView.activeNativeDragDelegateMarker = nil + outlineView.activeNativeDragOwnerships = [] + outlineView.activeNativeDragOwnership = nil + outlineView.activeNativeDragSession = nil + } + ) + // The ordered list mirrors AppKit's pasteboard item order; use + // its first writer as the canonical source identity. + let promotedWriter = promotedWriters.first ?? fallbackWriter + let promotedOwnerships = pendingPreviewDrag.promote(writers: promotedWriters) + promotedWriter?.materializeRegisteredPayload(to: session.draggingPasteboard) + let ownerships = promotedOwnerships.isEmpty + ? (promotedWriter?.nativeDragOwnership()).map { [$0] } ?? [] + : promotedOwnerships + outlineView.activeNativeDragDelegateMarker = self + outlineView.activeNativeDragSession = session + outlineView.activeNativeDragWriter = promotedWriter + outlineView.activeNativeDragOwnerships = ownerships + outlineView.pendingNativeDragWriter = nil + outlineView.pendingNativeDragTokenID = nil + trackNativeDrag( + sourceView: outlineView, + session: session, + writer: promotedWriter, + ownerships: ownerships + ) + } + } + + func outlineView( + _ outlineView: NSOutlineView, + draggingSession session: NSDraggingSession, + endedAt screenPoint: NSPoint, + operation: NSDragOperation + ) { + guard let outlineView = outlineView as? FileExplorerNSOutlineView, + outlineView.activeNativeDragSession === session else { + // The delegate may have been rebuilt between writer creation + // and the terminal callback. Use this session's own pasteboard + // for idempotent capability cleanup; never inspect the + // process-wide board here. + FilePreviewDragPasteboardWriter.discardRegisteredDrag(from: session) + return + } + if !outlineView.activeNativeDragOwnerships.isEmpty { + for ownership in outlineView.activeNativeDragOwnerships { + ownership.finish(from: session.draggingPasteboard) + } + } else { + // The matching session identity proves this is not a stale + // callback. Keep a compatibility fallback for an AppKit path + // that released the plain writer before promotion. + FilePreviewDragPasteboardWriter.discardRegisteredDrag(from: session) + } + outlineView.activeNativeDragDelegateMarker = nil + outlineView.activeNativeDragWriter?.releaseSourceGraph() + outlineView.activeNativeDragWriter = nil + outlineView.activeNativeDragOwnerships = [] + outlineView.activeNativeDragOwnership = nil + outlineView.activeNativeDragSession = nil + forgetTrackedNativeDrag(matching: session) + } + + /// Reclaims an outline drag at the next pointer boundary when AppKit + /// omitted its native terminal callback during reconstruction. The + /// exact outline argument matters because ``Coordinator.outlineView`` + /// may already point at a newly built view. + func prepareForNativeDragBoundary(on outlineView: NSOutlineView) { + guard let outlineView = outlineView as? FileExplorerNSOutlineView else { return } + if reclaimTrackedNativeDrag() { + // The tracked source may be an older outline retained by the + // writer. Clear only this view's pending request; its active + // state, if any, belongs to a separate generation. + pendingPreviewDrag.finishPending() + outlineView.pendingNativeDragWriter = nil + outlineView.pendingNativeDragTokenID = nil + return + } + guard let session = outlineView.activeNativeDragSession else { + outlineView.activeNativeDragDelegateMarker = nil + pendingPreviewDrag.finishPending() + outlineView.pendingNativeDragWriter = nil + if let tokenID = outlineView.pendingNativeDragTokenID { + pendingPreviewDrag.remove(tokenID: tokenID) + } + outlineView.pendingNativeDragTokenID = nil + outlineView.activeNativeDragOwnership = nil + return + } + for ownership in outlineView.activeNativeDragOwnerships { + ownership.finish(from: session.draggingPasteboard) + } + pendingPreviewDrag.finishPending() + outlineView.activeNativeDragDelegateMarker = nil + outlineView.pendingNativeDragWriter = nil + outlineView.pendingNativeDragTokenID = nil + outlineView.activeNativeDragWriter?.releaseSourceGraph() + outlineView.activeNativeDragWriter = nil + outlineView.activeNativeDragOwnerships = [] + outlineView.activeNativeDragOwnership = nil + outlineView.activeNativeDragSession = nil + } +} diff --git a/Sources/FileExplorerOutlineCoordinator.swift b/Sources/FileExplorerOutlineCoordinator.swift new file mode 100644 index 000000000000..2e7b97b4ceaf --- /dev/null +++ b/Sources/FileExplorerOutlineCoordinator.swift @@ -0,0 +1,630 @@ +import AppKit +import CmuxFileTree +import SwiftUI + +extension FileExplorerPanelView { + /// Data source, delegate and tree observer for the Files outline. + /// + /// Rows are the store's ``FileExplorerNode`` objects, so child lookups are + /// `O(1)` and a refresh that keeps a path keeps its row. Store diffs become + /// `removeItems`/`insertItems` batches; nothing here calls `reloadData` + /// except a full reset (new root or provider). + @MainActor + final class Coordinator: NSObject, NSOutlineViewDataSource, NSOutlineViewDelegate, NSMenuDelegate, + FileExplorerTreeObserving { + var store: FileExplorerStore { + didSet { + guard oldValue !== store else { return } + oldValue.removeTreeObserver(self) + store.addTreeObserver(self) + rebuildOutline() + } + } + var state: FileExplorerState + var onOpenFilePreview: (String) -> Void + var placement: FileExplorerPanelPlacement + var onFocus: (() -> Void)? + var onContainerChange: ((FileExplorerContainerView?) -> Void)? + weak var containerView: FileExplorerContainerView? + weak var outlineView: NSOutlineView? { + didSet { + guard oldValue !== outlineView else { return } + rebuildOutline() + } + } + let iconCache = FileExplorerIconCache() + var isUpdatingOutlineProgrammatically = false + private var styleObserver: Any? + private var scrollEndObserver: Any? + weak var renamingCell: FileExplorerCellView? + var renamingPath: String? + // Keep one coordinator-level record for the promoted native source. + // The source view can be replaced during SwiftUI reconstruction, so + // view-local markers alone cannot reclaim a lost endedAt callback. + weak var activeNativeDragSourceView: NSView? + weak var activeNativeDragWriter: FilePreviewDragPasteboardWriter? + var activeNativeDragSession: NSDraggingSession? + var activeNativeDragOwnerships: [FilePreviewNativeDragOwnership] = [] + lazy var pendingPreviewDrag = FilePreviewNativeDragPendingOwnership { [weak self] tokenID in + self?.previewWriterDidDeallocate(tokenID: tokenID) + } + + init( + store: FileExplorerStore, + state: FileExplorerState, + onOpenFilePreview: @escaping (String) -> Void, + placement: FileExplorerPanelPlacement = .rightSidebar, + onFocus: (() -> Void)? = nil, + onContainerChange: ((FileExplorerContainerView?) -> Void)? = nil + ) { + self.store = store + self.state = state + self.onOpenFilePreview = onOpenFilePreview + self.placement = placement + self.onFocus = onFocus + self.onContainerChange = onContainerChange + super.init() + store.addTreeObserver(self) + styleObserver = NotificationCenter.default.addObserver( + forName: .fileExplorerStyleDidChange, object: nil, queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { + self?.applyStyleChange() + } + } + scrollEndObserver = NotificationCenter.default.addObserver( + forName: NSScrollView.didEndLiveScrollNotification, object: nil, queue: .main + ) { [weak self] notification in + MainActor.assumeIsolated { + guard let self, let scrollView = notification.object as? NSScrollView, + scrollView === self.outlineView?.enclosingScrollView else { return } + self.store.scheduleViewStateSave() + } + } + } + + deinit { + if let styleObserver { NotificationCenter.default.removeObserver(styleObserver) } + if let scrollEndObserver { NotificationCenter.default.removeObserver(scrollEndObserver) } + } + + @MainActor + @discardableResult + func handleModeShortcut(_ mode: RightSidebarMode, in window: NSWindow?) -> Bool { + guard placement == .rightSidebar else { return false } + _ = AppDelegate.shared?.focusRightSidebarInActiveMainWindow( + mode: mode, + focusFirstItem: true, + preferredWindow: window + ) + return true + } + + @MainActor + func noteKeyboardFocus(mode: RightSidebarMode, in window: NSWindow?) { + switch placement { + case .rightSidebar: + guard let window else { return } + AppDelegate.shared?.noteRightSidebarKeyboardFocusIntent(mode: mode, in: window) + case .pane: + onFocus?() + } + } + + /// Updates root-level chrome from SwiftUI. Rows never reload here. + func reloadIfNeeded() { + containerView?.updateVisibility( + hasContent: !store.rootPath.isEmpty, + isLoading: store.isRootLoading, + statusMessage: store.rootStatusMessage, + showsRemoteTarget: store.provider is any RemoteFileExplorerProvider + ) + } + + // MARK: - Full rebuild + + /// Reloads rows from the store and re-applies expansion and selection. + /// Runs only when the outline or store is replaced or the tree resets. + func rebuildOutline() { + guard let outlineView else { return } + let style = FileExplorerStyle.current + if outlineView.rowHeight != style.rowHeight { outlineView.rowHeight = style.rowHeight } + withProgrammaticOutlineUpdate { + outlineView.reloadData() + expandStoredExpansion(in: store.rootNodes, outlineView: outlineView) + applyStoredSelection(in: outlineView, fallbackToFirstVisible: false, scroll: false) + } + } + + private func expandStoredExpansion(in nodes: [FileExplorerNode], outlineView: NSOutlineView) { + for node in nodes where node.isDirectory && store.expandedPaths.contains(node.path) { + if !outlineView.isItemExpanded(node) { outlineView.expandItem(node) } + if let children = node.children { + expandStoredExpansion(in: children, outlineView: outlineView) + } + } + } + + private func applyStyleChange() { + guard let outlineView else { return } + let style = FileExplorerStyle.current + outlineView.indentationPerLevel = style.indentation + outlineView.rowHeight = style.rowHeight + reconfigureVisibleRows() + } + + /// Reconfigures the cells on screen, for style, font or appearance changes. + func reconfigureVisibleRows() { + guard let outlineView else { return } + let range = outlineView.rows(in: outlineView.visibleRect) + guard range.location != NSNotFound else { return } + for row in range.location..