diff --git a/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Billing/ProUpgradeSource.swift b/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Billing/ProUpgradeSource.swift index 3ca51b717ca3..4bf8574b4312 100644 --- a/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Billing/ProUpgradeSource.swift +++ b/Packages/macOS/CmuxCloud/Sources/CmuxCloud/Billing/ProUpgradeSource.swift @@ -22,6 +22,8 @@ public enum ProUpgradeSource: String, CaseIterable, Sendable { case settingsCloudMachines = "mac_settings_cloud_machines" /// Machines panel empty state: plan does not include Cloud machines. case machinesPanelRequiresPro = "mac_machines_panel_requires_pro" + /// "Introducing cmux Cloud" welcome window, Free plan. + case cloudWelcome = "mac_cloud_welcome" /// Machines panel nudge under the create button. case machinesPanelUpgradeNudge = "mac_machines_panel_upgrade_nudge" /// Machines panel free-access countdown / expired banner. diff --git a/Packages/macOS/CmuxCloud/Sources/CmuxCloud/BillingPlan.swift b/Packages/macOS/CmuxCloud/Sources/CmuxCloud/BillingPlan.swift new file mode 100644 index 000000000000..6f1a9fe8e00c --- /dev/null +++ b/Packages/macOS/CmuxCloud/Sources/CmuxCloud/BillingPlan.swift @@ -0,0 +1,104 @@ +import Foundation + +/// The account-scoped billing entitlement used by Cloud access controls. +public struct BillingPlanState: Sendable, Equatable { + /// The account whose entitlement is known, or `nil` while unknown. + public let accountID: String? + /// Whether the known account includes Cloud access. + public let isPro: Bool + /// Whether the known account can manage billing through the hosted portal. + public let canManageBilling: Bool + + /// Creates an entitlement snapshot. + /// + /// - Parameters: + /// - accountID: The account owning the snapshot, or `nil` when unknown. + /// - isPro: Whether the account includes Cloud access. + /// - canManageBilling: Whether billing can be managed in the hosted portal. + public init(accountID: String?, isPro: Bool, canManageBilling: Bool) { + self.accountID = accountID + self.isPro = isPro + self.canManageBilling = canManageBilling + } + + /// An unknown entitlement that must not be treated as a free plan. + public static var unknown: Self { Self(accountID: nil, isPro: false, canManageBilling: false) } + + /// Applies a successful response to the account that requested it. + public func applyingSuccess(for accountID: String, isPro: Bool, canManageBilling: Bool) -> Self { + Self(accountID: accountID, isPro: isPro, canManageBilling: canManageBilling) + } + + /// Retains an existing answer for the same account and clears other answers. + public func applyingFailure(for accountID: String) -> Self { + self.accountID == accountID ? self : .unknown + } +} + +/// The decoded billing response returned by the Cloud service. +public struct BillingPlanDetails: Sendable, Equatable { + /// Whether the account includes Cloud access. + public let isPro: Bool + /// Whether the account can manage billing through the hosted portal. + public let canManageBilling: Bool + + /// Creates decoded billing details. + public init(isPro: Bool, canManageBilling: Bool) { + self.isPro = isPro + self.canManageBilling = canManageBilling + } +} + +/// Fetches a billing entitlement without requiring UI isolation. +public struct BillingPlanClient: Sendable { + /// Creates a client that uses the supplied URL session. + public init(session: URLSession = .shared) { + self.session = session + } + + /// Fetches and decodes the entitlement for an authenticated account. + /// + /// - Parameters: + /// - url: The billing-plan endpoint. + /// - accessToken: An optional bearer token. + /// - refreshToken: An optional session refresh token. + /// - Returns: The decoded entitlement details. + /// - Throws: A URL-loading or decoding error when the request fails. + public func fetch(from url: URL, accessToken: String?, refreshToken: String? = nil) async throws -> BillingPlanDetails { + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.timeoutInterval = 15 + request.setValue("application/json", forHTTPHeaderField: "Accept") + if let accessToken { + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + } + if let refreshToken { + request.setValue(refreshToken, forHTTPHeaderField: "X-Stack-Refresh-Token") + } + let (data, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { + throw URLError(.badServerResponse) + } + let decoded = try JSONDecoder().decode(Response.self, from: data) + // The default endpoint returns both personal and active-team plans. + // A paid team grants Cloud access even when the personal subscription + // is free, which is the normal path for team-owned machines. + let paidPlanIDs = ["go", "pro", "max", "team", "founders"] + let isPro = decoded.isPro == true + || paidPlanIDs.contains(decoded.planId?.lowercased() ?? "") + || paidPlanIDs.contains(decoded.teamPlanId?.lowercased() ?? "") + let canManageBilling = decoded.billingManagement == "stripe" + || decoded.teamBillingManagement == "stripe" + return BillingPlanDetails(isPro: isPro, canManageBilling: canManageBilling) + } + + private let session: URLSession + + private struct Response: Decodable { + let isPro: Bool? + let planId: String? + let billingManagement: String? + let teamPlanId: String? + let teamBillingManagement: String? + } +} diff --git a/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/BillingPlanTests.swift b/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/BillingPlanTests.swift new file mode 100644 index 000000000000..8d7a69fbbd34 --- /dev/null +++ b/Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/BillingPlanTests.swift @@ -0,0 +1,29 @@ +import CmuxCloud +import Testing + +@Suite("Billing plan state") +struct BillingPlanTests { + @Test("successful response is scoped to its account") + func successScopesAccount() { + let state = BillingPlanState.unknown.applyingSuccess( + for: "account-a", + isPro: true, + canManageBilling: true + ) + #expect(state.accountID == "account-a") + #expect(state.isPro) + #expect(state.canManageBilling) + } + + @Test("same-account failure preserves the last known answer") + func sameAccountFailurePreservesAnswer() { + let state = BillingPlanState(accountID: "account-a", isPro: true, canManageBilling: true) + #expect(state.applyingFailure(for: "account-a") == state) + } + + @Test("different-account failure clears the answer") + func differentAccountFailureClearsAnswer() { + let state = BillingPlanState(accountID: "account-a", isPro: true, canManageBilling: true) + #expect(state.applyingFailure(for: "account-b") == .unknown) + } +} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/CloudMachinesSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/CloudMachinesSection.swift index 8e1ecd1c81ad..452758e08f4b 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/CloudMachinesSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/CloudMachinesSection.swift @@ -64,6 +64,7 @@ public struct CloudMachinesSection: View { return } let includesCloud = await hostActions.cloudMachinesPlanIncludesCloud() + // A newer check (account switch, app reactivation) owns the row now. guard !Task.isCancelled else { return } planIncludesCloud = includesCloud } diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 7ae5c8d1f97f..caa2d7a1ac1a 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -232456,881 +232456,6 @@ } } }, - "cloudVM.error.resourcePool.action": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Pauzirajte ili izbrišite VM, ili pređite na Max." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Sæt en VM på pause eller slet den, eller opgrader til Max." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Metti in pausa o elimina una VM, oppure passa a Max." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ផ្អាក ឬលុប VM មួយ ឬដំឡើងទៅ Max។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Sett en VM på pause eller slett den, eller oppgrader til Max." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wstrzymaj lub usuń VM albo przejdź na Max." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Pause ou exclua uma VM, ou faça upgrade para o Max." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Приостановите или удалите VM либо перейдите на Max." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "หยุด VM ชั่วคราวหรือลบ VM หรืออัปเกรดเป็น Max" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bir VM'yi duraklatın veya silin ya da Max'e yükseltin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Призупиніть або видаліть VM чи перейдіть на Max." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Pause or delete a VM, or upgrade to Max." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Pausiere oder lösche eine VM, oder führe ein Upgrade auf Max durch." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mettez en pause ou supprimez une VM, ou passez à Max." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "أوقف جهازًا افتراضيًا مؤقتًا أو احذفه، أو قم بالترقية إلى Max." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Pausa o elimina una VM, o actualiza a Max." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "請暫停或刪除一台虛擬機,或升級至 Max。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "请暂停或删除一台虚拟机,或升级到 Max。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "VM을 일시 중지하거나 삭제하거나 Max로 업그레이드하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "VM を一時停止するか削除する、または Max にアップグレードしてください。" - } - } - } - }, - "cloudVM.error.resourcePool.maxAction": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Pauzirajte ili izbrišite VM." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Sæt en VM på pause eller slet den." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Metti in pausa o elimina una VM." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ផ្អាក ឬលុប VM មួយ។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Sett en VM på pause eller slett den." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wstrzymaj lub usuń VM." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Pause ou exclua uma VM." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Приостановите или удалите VM." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "หยุด VM ชั่วคราวหรือลบ VM" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bir VM'yi duraklatın veya silin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Призупиніть або видаліть VM." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Pause or delete a VM." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Pausiere oder lösche eine VM." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Mettez en pause ou supprimez une VM." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "أوقف جهازًا افتراضيًا مؤقتًا أو احذفه." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Pausa o elimina una VM." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "請暫停或刪除一台虛擬機。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "请暂停或删除一台虚拟机。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "VM을 일시 중지하거나 삭제하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "VM を一時停止するか削除してください。" - } - } - } - }, - "cloud.machines.pool.usage": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "%1$lld od %2$lld vCPU-a · %3$lld od %4$lld GB RAM-a u upotrebi" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "%1$lld af %2$lld vCPU'er · %3$lld af %4$lld GB RAM i brug" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "%1$lld di %2$lld vCPU · %3$lld di %4$lld GB di RAM in uso" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "កំពុងប្រើ vCPU %1$lld ក្នុងចំណោម %2$lld · RAM %3$lld ក្នុងចំណោម %4$lld GB" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "%1$lld av %2$lld vCPU-er · %3$lld av %4$lld GB RAM i bruk" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "W użyciu: %1$lld z %2$lld vCPU · %3$lld z %4$lld GB RAM" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "%1$lld de %2$lld vCPUs · %3$lld de %4$lld GB de RAM em uso" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Используется %1$lld из %2$lld vCPU · %3$lld из %4$lld ГБ RAM" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ใช้อยู่ %1$lld จาก %2$lld vCPU · RAM %3$lld จาก %4$lld GB" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "%2$lld vCPU'nun %1$lld tanesi · %4$lld GB RAM'in %3$lld GB'ı kullanımda" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Використано %1$lld з %2$lld vCPU · %3$lld з %4$lld ГБ RAM" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$lld of %2$lld vCPUs · %3$lld of %4$lld GB RAM in use" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "%1$lld von %2$lld vCPUs · %3$lld von %4$lld GB RAM belegt" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "%1$lld vCPU sur %2$lld · %3$lld Go de RAM sur %4$lld utilisés" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "قيد الاستخدام: %1$lld من %2$lld وحدة معالجة مركزية افتراضية · %3$lld من %4$lld جيجابايت من الذاكرة" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "%1$lld de %2$lld vCPU · %3$lld de %4$lld GB de RAM en uso" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "已使用 %1$lld / %2$lld 個虛擬 CPU · %3$lld / %4$lld GB 記憶體" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已使用 %1$lld / %2$lld 个虚拟 CPU · %3$lld / %4$lld GB 内存" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사용 중: vCPU %1$lld/%2$lld개 · RAM %3$lld/%4$lld GB" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "使用中: %1$lld / %2$lld vCPU · %3$lld / %4$lld GB RAM" - } - } - } - }, - "cloud.newMachine.pool.doesNotFit": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ova veličina treba %1$lld GB RAM-a, ali je od vašeg skupa od %3$lld GB slobodno samo %2$lld GB. Pauzirajte ili izbrišite VM, ili pređite na Max." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelse kræver %1$lld GB RAM, men kun %2$lld GB af din pulje på %3$lld GB er ledig. Sæt en VM på pause eller slet den, eller opgrader til Max." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Questa dimensione richiede %1$lld GB di RAM, ma del tuo pool da %3$lld GB sono liberi solo %2$lld GB. Metti in pausa o elimina una VM, oppure passa a Max." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ទំហំនេះត្រូវការ RAM %1$lld GB ប៉ុន្តែក្នុងបណ្តុំ %3$lld GB របស់អ្នក នៅទំនេរតែ %2$lld GB ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ ឬដំឡើងទៅ Max។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelsen trenger %1$lld GB RAM, men bare %2$lld GB av ressurspoolen din på %3$lld GB er ledig. Sett en VM på pause eller slett den, eller oppgrader til Max." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ten rozmiar wymaga %1$lld GB RAM, ale z Twojej puli %3$lld GB wolne jest tylko %2$lld GB. Wstrzymaj lub usuń VM albo przejdź na Max." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Este tamanho precisa de %1$lld GB de RAM, mas só %2$lld GB do seu pool de %3$lld GB estão livres. Pause ou exclua uma VM, ou faça upgrade para o Max." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Для этого размера нужно %1$lld ГБ RAM, но в вашем пуле на %3$lld ГБ свободно только %2$lld ГБ. Приостановите или удалите VM либо перейдите на Max." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ขนาดนี้ต้องใช้ RAM %1$lld GB แต่พูล %3$lld GB ของคุณว่างอยู่เพียง %2$lld GB หยุด VM ชั่วคราวหรือลบ VM หรืออัปเกรดเป็น Max" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bu boyut %1$lld GB RAM gerektiriyor, ancak %3$lld GB'lık havuzunuzun yalnızca %2$lld GB'ı boş. Bir VM'yi duraklatın veya silin ya da Max'e yükseltin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Для цього розміру потрібно %1$lld ГБ RAM, але у вашому пулі на %3$lld ГБ вільно лише %2$lld ГБ. Призупиніть або видаліть VM чи перейдіть на Max." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This size needs %1$lld GB RAM, but only %2$lld GB of your %3$lld GB pool is free. Pause or delete a VM, or upgrade to Max." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Diese Größe benötigt %1$lld GB RAM, aber von deinem %3$lld-GB-Pool sind nur %2$lld GB frei. Pausiere oder lösche eine VM, oder führe ein Upgrade auf Max durch." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cette taille nécessite %1$lld Go de RAM, mais seuls %2$lld Go de votre réserve de %3$lld Go sont libres. Mettez en pause ou supprimez une VM, ou passez à Max." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يحتاج هذا الحجم إلى %1$lld جيجابايت من الذاكرة، لكن المتاح من مجموعتك البالغة %3$lld جيجابايت هو %2$lld جيجابايت فقط. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه، أو قم بالترقية إلى Max." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Este tamaño necesita %1$lld GB de RAM, pero solo quedan libres %2$lld GB de tu grupo de %3$lld GB. Pausa o elimina una VM, o actualiza a Max." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此規格需要 %1$lld GB 記憶體,但你的 %3$lld GB 資源池只剩 %2$lld GB 可用。請暫停或刪除一台虛擬機,或升級至 Max。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此规格需要 %1$lld GB 内存,但你的 %3$lld GB 资源池只剩 %2$lld GB 可用。请暂停或删除一台虚拟机,或升级到 Max。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이 크기에는 RAM %1$lld GB가 필요하지만 %3$lld GB 풀 중 %2$lld GB만 남아 있습니다. VM을 일시 중지하거나 삭제하거나 Max로 업그레이드하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "このサイズには %1$lld GB RAM が必要ですが、%3$lld GB のプールのうち空きは %2$lld GB だけです。VM を一時停止するか削除する、または Max にアップグレードしてください。" - } - } - } - }, - "cloud.newMachine.pool.doesNotFitMax": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ova veličina treba %1$lld GB RAM-a, ali je od vašeg skupa od %3$lld GB slobodno samo %2$lld GB. Pauzirajte ili izbrišite VM." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelse kræver %1$lld GB RAM, men kun %2$lld GB af din pulje på %3$lld GB er ledig. Sæt en VM på pause eller slet den." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Questa dimensione richiede %1$lld GB di RAM, ma del tuo pool da %3$lld GB sono liberi solo %2$lld GB. Metti in pausa o elimina una VM." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ទំហំនេះត្រូវការ RAM %1$lld GB ប៉ុន្តែក្នុងបណ្តុំ %3$lld GB របស់អ្នក នៅទំនេរតែ %2$lld GB ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelsen trenger %1$lld GB RAM, men bare %2$lld GB av ressurspoolen din på %3$lld GB er ledig. Sett en VM på pause eller slett den." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ten rozmiar wymaga %1$lld GB RAM, ale z Twojej puli %3$lld GB wolne jest tylko %2$lld GB. Wstrzymaj lub usuń VM." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Este tamanho precisa de %1$lld GB de RAM, mas só %2$lld GB do seu pool de %3$lld GB estão livres. Pause ou exclua uma VM." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Для этого размера нужно %1$lld ГБ RAM, но в вашем пуле на %3$lld ГБ свободно только %2$lld ГБ. Приостановите или удалите VM." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ขนาดนี้ต้องใช้ RAM %1$lld GB แต่พูล %3$lld GB ของคุณว่างอยู่เพียง %2$lld GB หยุด VM ชั่วคราวหรือลบ VM" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bu boyut %1$lld GB RAM gerektiriyor, ancak %3$lld GB'lık havuzunuzun yalnızca %2$lld GB'ı boş. Bir VM'yi duraklatın veya silin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Для цього розміру потрібно %1$lld ГБ RAM, але у вашому пулі на %3$lld ГБ вільно лише %2$lld ГБ. Призупиніть або видаліть VM." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This size needs %1$lld GB RAM, but only %2$lld GB of your %3$lld GB pool is free. Pause or delete a VM." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Diese Größe benötigt %1$lld GB RAM, aber von deinem %3$lld-GB-Pool sind nur %2$lld GB frei. Pausiere oder lösche eine VM." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cette taille nécessite %1$lld Go de RAM, mais seuls %2$lld Go de votre réserve de %3$lld Go sont libres. Mettez en pause ou supprimez une VM." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يحتاج هذا الحجم إلى %1$lld جيجابايت من الذاكرة، لكن المتاح من مجموعتك البالغة %3$lld جيجابايت هو %2$lld جيجابايت فقط. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Este tamaño necesita %1$lld GB de RAM, pero solo quedan libres %2$lld GB de tu grupo de %3$lld GB. Pausa o elimina una VM." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此規格需要 %1$lld GB 記憶體,但你的 %3$lld GB 資源池只剩 %2$lld GB 可用。請暫停或刪除一台虛擬機。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此规格需要 %1$lld GB 内存,但你的 %3$lld GB 资源池只剩 %2$lld GB 可用。请暂停或删除一台虚拟机。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이 크기에는 RAM %1$lld GB가 필요하지만 %3$lld GB 풀 중 %2$lld GB만 남아 있습니다. VM을 일시 중지하거나 삭제하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "このサイズには %1$lld GB RAM が必要ですが、%3$lld GB のプールのうち空きは %2$lld GB だけです。VM を一時停止するか削除してください。" - } - } - } - }, - "cloud.newMachine.pool.doesNotFitVcpus": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ova veličina treba %1$lld vCPU-a, ali je od vaših %3$lld vCPU-a slobodno samo %2$lld. Pauzirajte ili izbrišite VM, ili pređite na Max." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelse kræver %1$lld vCPU'er, men kun %2$lld af dine %3$lld vCPU'er er ledige. Sæt en VM på pause eller slet den, eller opgrader til Max." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Questa dimensione richiede %1$lld vCPU, ma delle tue %3$lld vCPU ne sono libere solo %2$lld. Metti in pausa o elimina una VM, oppure passa a Max." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ទំហំនេះត្រូវការ vCPU %1$lld ប៉ុន្តែក្នុងចំណោម vCPU %3$lld របស់អ្នក នៅទំនេរតែ %2$lld ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ ឬដំឡើងទៅ Max។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelsen trenger %1$lld vCPU-er, men bare %2$lld av dine %3$lld vCPU-er er ledige. Sett en VM på pause eller slett den, eller oppgrader til Max." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ten rozmiar wymaga %1$lld vCPU, ale z Twoich %3$lld vCPU wolne jest tylko %2$lld. Wstrzymaj lub usuń VM albo przejdź na Max." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Este tamanho precisa de %1$lld vCPUs, mas só %2$lld das suas %3$lld vCPUs estão livres. Pause ou exclua uma VM, ou faça upgrade para o Max." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Для этого размера нужно %1$lld vCPU, но из ваших %3$lld vCPU свободно только %2$lld. Приостановите или удалите VM либо перейдите на Max." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ขนาดนี้ต้องใช้ %1$lld vCPU แต่จาก %3$lld vCPU ของคุณว่างอยู่เพียง %2$lld หยุด VM ชั่วคราวหรือลบ VM หรืออัปเกรดเป็น Max" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bu boyut %1$lld vCPU gerektiriyor, ancak %3$lld vCPU'nuzun yalnızca %2$lld tanesi boş. Bir VM'yi duraklatın veya silin ya da Max'e yükseltin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Для цього розміру потрібно %1$lld vCPU, але з ваших %3$lld vCPU вільно лише %2$lld. Призупиніть або видаліть VM чи перейдіть на Max." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This size needs %1$lld vCPUs, but only %2$lld of your %3$lld vCPUs are free. Pause or delete a VM, or upgrade to Max." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Diese Größe benötigt %1$lld vCPUs, aber von deinen %3$lld vCPUs sind nur %2$lld frei. Pausiere oder lösche eine VM, oder führe ein Upgrade auf Max durch." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cette taille nécessite %1$lld vCPU, mais seuls %2$lld de vos %3$lld vCPU sont libres. Mettez en pause ou supprimez une VM, ou passez à Max." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يحتاج هذا الحجم إلى %1$lld من وحدات المعالجة المركزية الافتراضية، لكن المتاح منها %2$lld فقط من أصل %3$lld. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه، أو قم بالترقية إلى Max." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Este tamaño necesita %1$lld vCPU, pero solo quedan libres %2$lld de tus %3$lld vCPU. Pausa o elimina una VM, o actualiza a Max." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此規格需要 %1$lld 個虛擬 CPU,但你的 %3$lld 個虛擬 CPU 中只剩 %2$lld 個可用。請暫停或刪除一台虛擬機,或升級至 Max。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此规格需要 %1$lld 个虚拟 CPU,但你的 %3$lld 个虚拟 CPU 中只剩 %2$lld 个可用。请暂停或删除一台虚拟机,或升级到 Max。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이 크기에는 vCPU %1$lld개가 필요하지만 vCPU %3$lld개 중 %2$lld개만 남아 있습니다. VM을 일시 중지하거나 삭제하거나 Max로 업그레이드하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "このサイズには %1$lld vCPU が必要ですが、%3$lld vCPU のうち空きは %2$lld vCPU だけです。VM を一時停止するか削除する、または Max にアップグレードしてください。" - } - } - } - }, - "cloud.newMachine.pool.doesNotFitVcpusMax": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Ova veličina treba %1$lld vCPU-a, ali je od vaših %3$lld vCPU-a slobodno samo %2$lld. Pauzirajte ili izbrišite VM." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelse kræver %1$lld vCPU'er, men kun %2$lld af dine %3$lld vCPU'er er ledige. Sæt en VM på pause eller slet den." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Questa dimensione richiede %1$lld vCPU, ma delle tue %3$lld vCPU ne sono libere solo %2$lld. Metti in pausa o elimina una VM." - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ទំហំនេះត្រូវការ vCPU %1$lld ប៉ុន្តែក្នុងចំណោម vCPU %3$lld របស់អ្នក នៅទំនេរតែ %2$lld ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ។" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Denne størrelsen trenger %1$lld vCPU-er, men bare %2$lld av dine %3$lld vCPU-er er ledige. Sett en VM på pause eller slett den." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ten rozmiar wymaga %1$lld vCPU, ale z Twoich %3$lld vCPU wolne jest tylko %2$lld. Wstrzymaj lub usuń VM." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Este tamanho precisa de %1$lld vCPUs, mas só %2$lld das suas %3$lld vCPUs estão livres. Pause ou exclua uma VM." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Для этого размера нужно %1$lld vCPU, но из ваших %3$lld vCPU свободно только %2$lld. Приостановите или удалите VM." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ขนาดนี้ต้องใช้ %1$lld vCPU แต่จาก %3$lld vCPU ของคุณว่างอยู่เพียง %2$lld หยุด VM ชั่วคราวหรือลบ VM" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bu boyut %1$lld vCPU gerektiriyor, ancak %3$lld vCPU'nuzun yalnızca %2$lld tanesi boş. Bir VM'yi duraklatın veya silin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Для цього розміру потрібно %1$lld vCPU, але з ваших %3$lld vCPU вільно лише %2$lld. Призупиніть або видаліть VM." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This size needs %1$lld vCPUs, but only %2$lld of your %3$lld vCPUs are free. Pause or delete a VM." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Diese Größe benötigt %1$lld vCPUs, aber von deinen %3$lld vCPUs sind nur %2$lld frei. Pausiere oder lösche eine VM." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cette taille nécessite %1$lld vCPU, mais seuls %2$lld de vos %3$lld vCPU sont libres. Mettez en pause ou supprimez une VM." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "يحتاج هذا الحجم إلى %1$lld من وحدات المعالجة المركزية الافتراضية، لكن المتاح منها %2$lld فقط من أصل %3$lld. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Este tamaño necesita %1$lld vCPU, pero solo quedan libres %2$lld de tus %3$lld vCPU. Pausa o elimina una VM." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "此規格需要 %1$lld 個虛擬 CPU,但你的 %3$lld 個虛擬 CPU 中只剩 %2$lld 個可用。請暫停或刪除一台虛擬機。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此规格需要 %1$lld 个虚拟 CPU,但你的 %3$lld 个虚拟 CPU 中只剩 %2$lld 个可用。请暂停或删除一台虚拟机。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이 크기에는 vCPU %1$lld개가 필요하지만 vCPU %3$lld개 중 %2$lld개만 남아 있습니다. VM을 일시 중지하거나 삭제하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "このサイズには %1$lld vCPU が必要ですが、%3$lld vCPU のうち空きは %2$lld vCPU だけです。VM を一時停止するか削除してください。" - } - } - } - }, "machines.empty.upgrade": { "extractionState": "manual", "localizations": { @@ -236656,381 +235781,6 @@ } } }, - "machines.new.plan.loading": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Loading your Cloud machine plan…" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ تحميل خطة جهاز Cloud…" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Učitavanje plana Cloud mašine…" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Indlæser din Cloud-maskineplan…" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Dein Cloud-Maschinentarif wird geladen…" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Cargando el plan de tu máquina Cloud…" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Chargement du forfait de votre machine Cloud…" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Caricamento del piano della macchina Cloud…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud マシンのプランを読み込んでいます…" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "កំពុងផ្ទុកគម្រោងម៉ាស៊ីន Cloud របស់អ្នក…" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Cloud 머신 요금제를 불러오는 중…" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Laster inn Cloud-maskinplanen din…" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Wczytywanie planu maszyny Cloud…" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Carregando o plano da sua máquina Cloud…" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Загрузка тарифа машины Cloud…" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "กำลังโหลดแผนเครื่อง Cloud ของคุณ…" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Cloud makine planınız yükleniyor…" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Завантаження тарифу машини Cloud…" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在加载你的 Cloud 机器方案…" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "正在載入您的 Cloud 機器方案…" - } - } - } - }, - "machines.new.plan.retry": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Retry" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعادة المحاولة" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Pokušaj ponovo" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Prøv igen" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Erneut versuchen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Reintentar" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Réessayer" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Riprova" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "再試行" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ព្យាយាមម្ដងទៀត" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "재시도" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Prøv igjen" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Ponów" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Tentar Novamente" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Повторить" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ลองใหม่" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Tekrar Dene" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Повторити" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重试" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重試" - } - } - } - }, - "machines.new.plan.error": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Cloud plan details are still loading. Retry in a moment." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لا تزال تفاصيل خطة Cloud قيد التحميل. أعد المحاولة بعد قليل." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Detalji Cloud plana se još učitavaju. Pokušajte ponovo za trenutak." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Cloud-planens detaljer indlæses stadig. Prøv igen om et øjeblik." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Details des Cloud-Tarifs werden noch geladen. Versuche es gleich noch einmal." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Los detalles del plan de Cloud aún se están cargando. Vuelve a intentarlo en un momento." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Les détails du forfait Cloud sont encore en cours de chargement. Réessayez dans un instant." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "I dettagli del piano Cloud sono ancora in caricamento. Riprova tra un momento." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud プランの詳細を読み込み中です。しばらくしてから再試行してください。" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ព័ត៌មានលម្អិតនៃគម្រោង Cloud នៅតែកំពុងផ្ទុក។ សូមព្យាយាមម្ដងទៀតបន្តិចទៀត។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Cloud 요금제 정보를 아직 불러오는 중입니다. 잠시 후 다시 시도하세요." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Detaljene for Cloud-planen lastes fortsatt inn. Prøv igjen om et øyeblikk." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Szczegóły planu Cloud wciąż się wczytują. Spróbuj ponownie za chwilę." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Os detalhes do plano Cloud ainda estão carregando. Tente novamente em instantes." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Сведения о тарифе Cloud ещё загружаются. Повторите попытку через мгновение." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รายละเอียดแผน Cloud ยังโหลดอยู่ โปรดลองใหม่อีกครั้งในอีกสักครู่" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Cloud planı ayrıntıları hâlâ yükleniyor. Birazdan tekrar deneyin." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Відомості про тариф Cloud ще завантажуються. Повторіть спробу через мить." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Cloud 方案详情仍在加载。请稍后重试。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "Cloud 方案詳細資料仍在載入。請稍後重試。" - } - } - } - }, "machines.new": { "extractionState": "manual", "localizations": { @@ -359867,131 +358617,6 @@ } } }, - "settings.search.alias.setting.terminal.copy-confirmation": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse تأكيد نسخ حافظة تحديد" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potvrda kopirano međuspremnik odabir" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekræftelse kopieret udklipsholder markering" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse Bestätigung kopiert Zwischenablage Auswahl" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmación copiado portapapeles selección" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmation copié presse-papiers sélection" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse conferma copiato appunti selezione" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 コピー クリップボード 選択" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse បញ្ជាក់ ចម្លង ក្ដារតម្រៀប ជ្រើស" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 확인 복사됨 클립보드 선택" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekreftelse kopiert utklippstavle markering" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potwierdzenie skopiowano schowek zaznaczenie" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmação copiado área de transferência seleção" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse подтверждение скопировано буфер обмена выделение" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse ยืนยัน คัดลอก คลิปบอร์ด เลือก" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse onay kopyalandı pano seçim" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse підтвердження скопійовано буфер обміну виділення" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 确认 已复制 剪贴板 选择" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 已複製 剪貼簿 選取" - } - } - } - }, "settings.search.alias.setting.terminal.reflow-hard-wrap-on-copy": { "extractionState": "manual", "localizations": { @@ -367499,256 +366124,6 @@ } } }, - "settings.terminal.showCopyConfirmation": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "إظهار تأكيد النسخ" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Prikaži potvrdu kopiranja" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Vis kopieringsbekræftelse" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Kopierbestätigung anzeigen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Show Copy Confirmation" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Mostrar confirmación de copia" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Afficher la confirmation de copie" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Mostra conferma di copia" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コピーの確認を表示" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "បង្ហាញការបញ្ជាក់ការចម្លង" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "복사 확인 표시" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Vis kopieringsbekreftelse" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Pokazuj potwierdzenie kopiowania" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Mostrar confirmação de cópia" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Показывать подтверждение копирования" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "แสดงการยืนยันการคัดลอก" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kopyalama onayını göster" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Показувати підтвердження копіювання" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示复制确认" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "顯示複製確認" - } - } - } - }, - "settings.terminal.showCopyConfirmation.subtitle": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يعرض «تم النسخ إلى الحافظة» لفترة وجيزة أسفل الطرفية عندما يؤدي تحديد النص إلى نسخه." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Kratko prikazuje „Kopirano u međuspremnik“ na dnu terminala kada se odabrani tekst kopira." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Viser kortvarigt „Kopieret til udklipsholder“ nederst i en terminal, når markering af tekst kopierer den." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Zeigt unten im Terminal kurz „In die Zwischenablage kopiert“ an, wenn durch Auswählen von Text kopiert wird." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Briefly shows “Copied to clipboard” at the bottom of a terminal when selecting text copies it." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Muestra brevemente «Copiado al portapapeles» en la parte inferior de una terminal cuando al seleccionar texto se copia." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Affiche brièvement « Copié dans le presse-papiers » en bas d’un terminal lorsque la sélection de texte le copie." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Mostra brevemente «Copiato negli appunti» in fondo a un terminale quando la selezione del testo lo copia." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "テキストを選択してコピーしたときに、ターミナルの下部に「クリップボードにコピーしました」を短く表示します。" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "បង្ហាញ «បានចម្លងទៅក្ដារតម្រៀប» មួយភ្លែតនៅខាងក្រោម terminal នៅពេលការជ្រើសអត្ថបទចម្លងវា។" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "텍스트를 선택해 복사하면 터미널 하단에 “클립보드에 복사됨”을 잠시 표시합니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Viser kort «Kopiert til utklippstavlen» nederst i en terminal når markering av tekst kopierer den." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Na chwilę wyświetla „Skopiowano do schowka” u dołu terminala, gdy zaznaczenie tekstu go kopiuje." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Mostra brevemente “Copiado para a área de transferência” na parte inferior do terminal quando selecionar texto o copia." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Ненадолго показывает «Скопировано в буфер обмена» внизу терминала, когда выделение текста копирует его." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "แสดง “คัดลอกไปยังคลิปบอร์ดแล้ว” ชั่วครู่ที่ด้านล่างของเทอร์มินัลเมื่อการเลือกข้อความคัดลอกข้อความนั้น" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Metin seçimi onu kopyaladığında terminalin altında kısa süreliğine “Panoya kopyalandı” gösterir." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Ненадовго показує «Скопійовано в буфер обміну» внизу термінала, коли виділення тексту копіює його." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择文本并复制时,在终端底部短暂显示“已复制到剪贴板”。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "選取文字並複製時,在終端機底部短暫顯示「已複製到剪貼簿」。" - } - } - } - }, "settings.terminal.reflowHardWrapOnCopy": { "extractionState": "manual", "localizations": { @@ -524797,506 +523172,6 @@ } } }, - "pricing.native.compare.resources": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "Resursi Cloud VM-ova" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM-ressourcer" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Risorse delle Cloud VM" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "ធនធាន Cloud VM" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM-ressurser" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Zasoby Cloud VM" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Recursos das Cloud VMs" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Ресурсы Cloud VM" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ทรัพยากร Cloud VM" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM kaynakları" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Ресурси Cloud VM" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM resources" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Cloud-VM-Ressourcen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ressources de VM cloud" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "موارد الجهاز الافتراضي السحابي" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Recursos de VM en la nube" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "雲端虛擬機資源" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM 资源" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM 리소스" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud VM のリソース" - } - } - } - }, - "pricing.native.compare.resources.standard": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU-a i 40 GB RAM-a, zajednički" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU'er og 40 GB RAM, delt" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU e 40 GB di RAM, condivisi" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "vCPU 20 និង RAM 40 GB ប្រើរួមគ្នា" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU-er og 40 GB RAM, delt" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU i 40 GB RAM, współdzielone" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "20 vCPUs e 40 GB de RAM, compartilhados" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU и 40 ГБ RAM, общие" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU และ RAM 40 GB ใช้ร่วมกัน" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU ve 40 GB RAM, paylaşımlı" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU і 40 ГБ RAM, спільні" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "20 vCPUs and 40 GB RAM, shared" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "20 vCPUs und 40 GB RAM, gemeinsam genutzt" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU et 40 Go de RAM partagés" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "20 وحدة معالجة مركزية افتراضية و40 جيجابايت من الذاكرة مشتركة" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU y 40 GB de RAM compartidos" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "共用 20 個虛擬 CPU 與 40 GB 記憶體" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "共享 20 个虚拟 CPU 和 40 GB 内存" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "vCPU 20개와 RAM 40 GB 공유" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU と 40 GB RAM を共有" - } - } - } - }, - "pricing.native.compare.resources.team": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU-a i 40 GB RAM-a po plaćenom mjestu, zajednički za cijeli tim" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU'er og 40 GB RAM pr. betalt plads, delt på tværs af teamet" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU e 40 GB di RAM per postazione a pagamento, condivisi da tutto il team" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "vCPU 20 និង RAM 40 GB ក្នុងមួយកៅអីបង់ប្រាក់ ដែលក្រុមទាំងមូលប្រើរួមគ្នា" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU-er og 40 GB RAM per betalt plass, delt på tvers av teamet" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU i 40 GB RAM na płatne miejsce, współdzielone w całym zespole" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "20 vCPUs e 40 GB de RAM por assento pago, compartilhados por toda a equipe" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU и 40 ГБ RAM на оплаченное место, общие для всей команды" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU และ RAM 40 GB ต่อที่นั่งแบบชำระเงิน ใช้ร่วมกันทั้งทีม" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Ücretli koltuk başına 20 vCPU ve 40 GB RAM, tüm ekipçe paylaşılır" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU і 40 ГБ RAM на оплачене місце, спільні для всієї команди" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "20 vCPUs and 40 GB RAM per paid seat, shared across the team" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "20 vCPUs und 40 GB RAM pro bezahltem Platz, im ganzen Team gemeinsam genutzt" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU et 40 Go de RAM par siège payant, partagés dans toute l’équipe" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "20 وحدة معالجة مركزية افتراضية و40 جيجابايت من الذاكرة لكل مقعد مدفوع، مشتركة على مستوى الفريق" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "20 vCPU y 40 GB de RAM por puesto de pago, compartidos en todo el equipo" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "每個付費席位 20 個虛擬 CPU 與 40 GB 記憶體,由整個團隊共用" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每个付费席位 20 个虚拟 CPU 和 40 GB 内存,由整个团队共享" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "유료 시트당 vCPU 20개와 RAM 40 GB, 팀 전체가 공유" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "有料シートごとに 20 vCPU と 40 GB RAM(チーム全体で共有)" - } - } - } - }, - "pricing.native.compare.resources.max": { - "extractionState": "manual", - "localizations": { - "bs": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU-a i 160 GB RAM-a, zajednički" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU'er og 160 GB RAM, delt" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU e 160 GB di RAM, condivisi" - } - }, - "km": { - "stringUnit": { - "state": "translated", - "value": "vCPU 80 និង RAM 160 GB ប្រើរួមគ្នា" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU-er og 160 GB RAM, delt" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU i 160 GB RAM, współdzielone" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "80 vCPUs e 160 GB de RAM, compartilhados" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU и 160 ГБ RAM, общие" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU และ RAM 160 GB ใช้ร่วมกัน" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU ve 160 GB RAM, paylaşımlı" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU і 160 ГБ RAM, спільні" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "80 vCPUs and 160 GB RAM, shared" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "80 vCPUs und 160 GB RAM, gemeinsam genutzt" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU et 160 Go de RAM partagés" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "80 وحدة معالجة مركزية افتراضية و160 جيجابايت من الذاكرة مشتركة" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU y 160 GB de RAM compartidos" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "共用 80 個虛擬 CPU 與 160 GB 記憶體" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "共享 80 个虚拟 CPU 和 160 GB 内存" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "vCPU 80개와 RAM 160 GB 공유" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "80 vCPU と 160 GB RAM を共有" - } - } - } - }, "machines.new.max.checkout": { "extractionState": "manual", "localizations": { @@ -613580,252 +611455,252 @@ } } }, - "cloud.enable.planChecking": { + "cloud.enable.title.enable": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Checking your cmux plan…" + "value": "Enable Cloud Machines" } }, "ar": { "stringUnit": { "state": "translated", - "value": "جارٍ التحقق من خطة cmux الخاصة بك…" + "value": "تمكين Cloud Machines" } }, - "de": { + "bs": { "stringUnit": { "state": "translated", - "value": "cmux-Plan wird geprüft…" + "value": "Omogući Cloud Machines" } }, - "es": { + "da": { "stringUnit": { "state": "translated", - "value": "Comprobando tu plan de cmux…" + "value": "Aktivér Cloud Machines" } }, - "fr": { + "de": { "stringUnit": { "state": "translated", - "value": "Vérification de votre forfait cmux…" + "value": "Cloud Machines aktivieren" } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "cmux プランを確認中…" + "value": "Habilitar Cloud Machines" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "cmux 요금제를 확인하는 중…" + "value": "Activer Cloud Machines" } }, - "zh-Hans": { + "it": { "stringUnit": { "state": "translated", - "value": "正在检查你的 cmux 方案…" + "value": "Abilita Cloud Machines" } }, - "zh-Hant": { + "ja": { "stringUnit": { "state": "translated", - "value": "正在檢查您的 cmux 方案…" + "value": "Cloud Machines を有効にする" } }, - "bs": { + "km": { "stringUnit": { "state": "translated", - "value": "Provjera vašeg cmux plana…" + "value": "បើក Cloud Machines" } }, - "da": { + "ko": { "stringUnit": { "state": "translated", - "value": "Tjekker din cmux-plan…" + "value": "Cloud Machines 활성화" } }, - "it": { + "nb": { "stringUnit": { "state": "translated", - "value": "Verifica del tuo piano cmux…" + "value": "Aktiver Cloud Machines" } }, - "km": { + "pl": { "stringUnit": { "state": "translated", - "value": "កំពុងពិនិត្យមើលគម្រោង cmux របស់អ្នក…" + "value": "Włącz Cloud Machines" } }, - "nb": { + "pt-BR": { "stringUnit": { "state": "translated", - "value": "Sjekker cmux-abonnementet ditt…" + "value": "Ativar Cloud Machines" } }, - "pl": { + "ru": { "stringUnit": { "state": "translated", - "value": "Sprawdzanie planu cmux…" + "value": "Включить Cloud Machines" } }, - "pt-BR": { + "th": { "stringUnit": { "state": "translated", - "value": "Verificando seu plano cmux…" + "value": "เปิดใช้ Cloud Machines" } }, - "ru": { + "tr": { "stringUnit": { "state": "translated", - "value": "Проверка вашего плана cmux…" + "value": "Cloud Machines'ı etkinleştir" } }, - "th": { + "uk": { "stringUnit": { "state": "translated", - "value": "กำลังตรวจสอบแผน cmux ของคุณ…" + "value": "Увімкнути Cloud Machines" } }, - "tr": { + "zh-Hans": { "stringUnit": { "state": "translated", - "value": "cmux planınız kontrol ediliyor…" + "value": "启用 Cloud Machines" } }, - "uk": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "Перевірка вашого плану cmux…" + "value": "啟用 Cloud Machines" } } } }, - "cloud.enable.planNote": { + "cloud.enable.planChecking": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Checking your cmux plan…" } }, "ar": { "stringUnit": { "state": "translated", - "value": "يتطلب خطة cmux مدفوعة." + "value": "جارٍ التحقق من خطة cmux الخاصة بك…" } }, "de": { "stringUnit": { "state": "translated", - "value": "Erfordert einen kostenpflichtigen cmux-Tarif." + "value": "cmux-Plan wird geprüft…" } }, "es": { "stringUnit": { "state": "translated", - "value": "Requiere un plan de pago de cmux." + "value": "Comprobando tu plan de cmux…" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Nécessite une offre cmux payante." + "value": "Vérification de votre forfait cmux…" } }, "ja": { "stringUnit": { "state": "translated", - "value": "cmux の有料プランが必要です。" + "value": "cmux プランを確認中…" } }, "ko": { "stringUnit": { "state": "translated", - "value": "유료 cmux 요금제가 필요합니다." + "value": "cmux 요금제를 확인하는 중…" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "需要付费的 cmux 套餐。" + "value": "正在检查你的 cmux 方案…" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "需要付費的 cmux 方案。" + "value": "正在檢查您的 cmux 方案…" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Provjera vašeg cmux plana…" } }, "da": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Tjekker din cmux-plan…" } }, "it": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Verifica del tuo piano cmux…" } }, "km": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "កំពុងពិនិត្យមើលគម្រោង cmux របស់អ្នក…" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Sjekker cmux-abonnementet ditt…" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Sprawdzanie planu cmux…" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Verificando seu plano cmux…" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Проверка вашего плана cmux…" } }, "th": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "กำลังตรวจสอบแผน cmux ของคุณ…" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "cmux planınız kontrol ediliyor…" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Requires a cmux Pro plan." + "value": "Перевірка вашого плану cmux…" } } } @@ -615080,6 +612955,131 @@ } } }, + "settings.cloudMachines.enable.title.enable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Enable Cloud Machines" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تمكين Cloud Machines" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Omogući Cloud Machines" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Aktivér Cloud Machines" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines aktivieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Habilitar Cloud Machines" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer Cloud Machines" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Abilita Cloud Machines" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines を有効にする" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បើក Cloud Machines" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines 활성화" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Aktiver Cloud Machines" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Włącz Cloud Machines" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativar Cloud Machines" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Включить Cloud Machines" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เปิดใช้ Cloud Machines" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Cloud Machines'ı etkinleştir" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Увімкнути Cloud Machines" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "启用 Cloud Machines" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "啟用 Cloud Machines" + } + } + } + }, "settings.cloudMachines.enable.cancel": { "extractionState": "manual", "localizations": { @@ -621014,6 +619014,2957 @@ } } }, + "debug.menu.showCloudWelcome": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Cloud Welcome…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار ترحيب Cloud…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Willkommen anzeigen…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar bienvenida de Cloud…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’accueil Cloud…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud のようこそ画面を表示…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 환영 화면 보기…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示 Cloud 欢迎界面…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示 Cloud 歡迎畫面…" + } + } + } + }, + "cloud.welcome.note.available": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud is available on cmux Pro and Max." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "Cloud متاح في cmux Pro وMax." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud ist mit cmux Pro und Max verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cloud está disponible en cmux Pro y Max." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cloud est disponible avec cmux Pro et Max." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud は cmux Pro と Max でご利用いただけます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud는 cmux Pro 및 Max에서 사용할 수 있습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "Cloud 适用于 cmux Pro 和 Max 方案。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "Cloud 適用於 cmux Pro 與 Max 方案。" + } + } + } + }, + "cloud.welcome.note.included": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Included in your plan." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مضمَّن في خطتك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In Ihrem Tarif enthalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Incluido en tu plan." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inclus dans votre forfait." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ご利用のプランに含まれています。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "현재 요금제에 포함되어 있습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "已包含在你的方案中。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已包含在您的方案中。" + } + } + } + }, + "cloud.welcome.setUp": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Set Up Cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعداد Cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud einrichten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Configurar Cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configurer Cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud を設定" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 설정" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "设置 Cloud" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "設定 Cloud" + } + } + } + }, + "cloud.welcome.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Introducing cmux cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نقدّم لكم cmux cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wir stellen vor: cmux cloud" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Presentamos cmux cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Découvrez cmux cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud のご紹介" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud를 소개합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "隆重推出 cmux cloud" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "隆重推出 cmux cloud" + } + } + } + }, + "cloud.welcome.newBadge": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جديد" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nuevo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新機能" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "신규" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新" + } + } + } + }, + "cloud.welcome.title.eyebrow": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Introducing" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نقدّم لكم" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wir stellen vor" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Presentamos" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Découvrez" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ご紹介" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "소개합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "隆重推出" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "隆重推出" + } + } + } + }, + "cloud.enable.reason.anyMac": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Opens on any Mac" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يفتح على أي جهاز Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffnet auf jedem Mac" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se abre en cualquier Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "S’ouvre sur n’importe quel 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 上開啟" + } + } + } + }, + "cloud.enable.reason.away": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Runs while you’re away" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يعمل أثناء غيابك" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Läuft weiter, während du weg bist" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sigue funcionando mientras no estás" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continue pendant votre absence" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "離れている間も動き続ける" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "자리를 비워도 계속 실행" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "离开时持续运行" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "離開時持續執行" + } + } + } + }, + "cloud.enable.reason.setUp": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stays set up" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يبقى جاهزًا" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bleibt eingerichtet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sigue configurada" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reste configurée" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "セットアップはそのまま" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "설정이 유지됨" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "配置保持不变" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "設定保持不變" + } + } + } + }, + "cloud.enable.reason.team": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shared with your team" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مشترك مع فريقك" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit deinem Team geteilt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compartida con tu equipo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Partagée avec votre équipe" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "チームと共有" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "팀과 공유" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "与团队共享" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "與團隊共用" + } + } + } + }, + "cloud.enable.reason.team.detail": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invite teammates, and share any port with a private link." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "ادعُ زملاءك وشارك أي منفذ برابط خاص." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lade Teammitglieder ein und teile jeden Port über einen privaten Link." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Invita a tu equipo y comparte cualquier puerto con un enlace privado." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Invitez vos coéquipiers et partagez n’importe quel port avec un lien privé." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "チームメンバーを招待して、どのポートもプライベートリンクで共有できます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "팀원을 초대하고 비공개 링크로 어떤 포트든 공유하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "邀请团队成员,并通过私有链接分享任意端口。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "邀請團隊成員,並透過私人連結分享任何連接埠。" + } + } + } + }, + "cloud.enable.intro.requiresPro": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Upgrade to use cmux cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "قم بالترقية لاستخدام cmux cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Upgrade, um cmux cloud zu nutzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mejora tu plan para usar cmux cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Passez à Pro pour utiliser cmux cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アップグレードして cmux cloud を使う" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "업그레이드하여 cmux cloud 사용하기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "升级以使用 cmux cloud" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "升級以使用 cmux cloud" + } + } + } + }, + "cloud.enable.intro.title": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use cmux cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدم cmux cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar cmux cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser cmux cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud を使う" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud 사용하기" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "使用 cmux cloud" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "使用 cmux cloud" + } + } + } + }, + "cloudVM.error.resourcePool.action": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pauzirajte ili izbrišite VM, ili pređite na Max." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Sæt en VM på pause eller slet den, eller opgrader til Max." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metti in pausa o elimina una VM, oppure passa a Max." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្អាក ឬលុប VM មួយ ឬដំឡើងទៅ Max។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Sett en VM på pause eller slett den, eller oppgrader til Max." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wstrzymaj lub usuń VM albo przejdź na Max." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pause ou exclua uma VM, ou faça upgrade para o Max." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Приостановите или удалите VM либо перейдите на Max." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "หยุด VM ชั่วคราวหรือลบ VM หรืออัปเกรดเป็น Max" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bir VM'yi duraklatın veya silin ya da Max'e yükseltin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Призупиніть або видаліть VM чи перейдіть на Max." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Pause or delete a VM, or upgrade to Max." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Pausiere oder lösche eine VM, oder führe ein Upgrade auf Max durch." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez en pause ou supprimez une VM, ou passez à Max." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أوقف جهازًا افتراضيًا مؤقتًا أو احذفه، أو قم بالترقية إلى Max." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pausa o elimina una VM, o actualiza a Max." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "請暫停或刪除一台虛擬機,或升級至 Max。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "请暂停或删除一台虚拟机,或升级到 Max。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "VM을 일시 중지하거나 삭제하거나 Max로 업그레이드하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "VM を一時停止するか削除する、または Max にアップグレードしてください。" + } + } + } + }, + "cloudVM.error.resourcePool.maxAction": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pauzirajte ili izbrišite VM." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Sæt en VM på pause eller slet den." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metti in pausa o elimina una VM." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្អាក ឬលុប VM មួយ។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Sett en VM på pause eller slett den." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wstrzymaj lub usuń VM." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pause ou exclua uma VM." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Приостановите или удалите VM." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "หยุด VM ชั่วคราวหรือลบ VM" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bir VM'yi duraklatın veya silin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Призупиніть або видаліть VM." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Pause or delete a VM." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Pausiere oder lösche eine VM." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez en pause ou supprimez une VM." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أوقف جهازًا افتراضيًا مؤقتًا أو احذفه." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pausa o elimina una VM." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "請暫停或刪除一台虛擬機。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "请暂停或删除一台虚拟机。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "VM을 일시 중지하거나 삭제하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "VM を一時停止するか削除してください。" + } + } + } + }, + "cloud.machines.pool.usage": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "%1$lld od %2$lld vCPU-a · %3$lld od %4$lld GB RAM-a u upotrebi" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "%1$lld af %2$lld vCPU'er · %3$lld af %4$lld GB RAM i brug" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%1$lld di %2$lld vCPU · %3$lld di %4$lld GB di RAM in uso" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កំពុងប្រើ vCPU %1$lld ក្នុងចំណោម %2$lld · RAM %3$lld ក្នុងចំណោម %4$lld GB" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "%1$lld av %2$lld vCPU-er · %3$lld av %4$lld GB RAM i bruk" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "W użyciu: %1$lld z %2$lld vCPU · %3$lld z %4$lld GB RAM" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%1$lld de %2$lld vCPUs · %3$lld de %4$lld GB de RAM em uso" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Используется %1$lld из %2$lld vCPU · %3$lld из %4$lld ГБ RAM" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ใช้อยู่ %1$lld จาก %2$lld vCPU · RAM %3$lld จาก %4$lld GB" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "%2$lld vCPU'nun %1$lld tanesi · %4$lld GB RAM'in %3$lld GB'ı kullanımda" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Використано %1$lld з %2$lld vCPU · %3$lld з %4$lld ГБ RAM" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$lld of %2$lld vCPUs · %3$lld of %4$lld GB RAM in use" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%1$lld von %2$lld vCPUs · %3$lld von %4$lld GB RAM belegt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%1$lld vCPU sur %2$lld · %3$lld Go de RAM sur %4$lld utilisés" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "قيد الاستخدام: %1$lld من %2$lld وحدة معالجة مركزية افتراضية · %3$lld من %4$lld جيجابايت من الذاكرة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%1$lld de %2$lld vCPU · %3$lld de %4$lld GB de RAM en uso" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已使用 %1$lld / %2$lld 個虛擬 CPU · %3$lld / %4$lld GB 記憶體" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "已使用 %1$lld / %2$lld 个虚拟 CPU · %3$lld / %4$lld GB 内存" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사용 중: vCPU %1$lld/%2$lld개 · RAM %3$lld/%4$lld GB" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "使用中: %1$lld / %2$lld vCPU · %3$lld / %4$lld GB RAM" + } + } + } + }, + "cloud.newMachine.pool.doesNotFit": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ova veličina treba %1$lld GB RAM-a, ali je od vašeg skupa od %3$lld GB slobodno samo %2$lld GB. Pauzirajte ili izbrišite VM, ili pređite na Max." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelse kræver %1$lld GB RAM, men kun %2$lld GB af din pulje på %3$lld GB er ledig. Sæt en VM på pause eller slet den, eller opgrader til Max." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa dimensione richiede %1$lld GB di RAM, ma del tuo pool da %3$lld GB sono liberi solo %2$lld GB. Metti in pausa o elimina una VM, oppure passa a Max." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ទំហំនេះត្រូវការ RAM %1$lld GB ប៉ុន្តែក្នុងបណ្តុំ %3$lld GB របស់អ្នក នៅទំនេរតែ %2$lld GB ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ ឬដំឡើងទៅ Max។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelsen trenger %1$lld GB RAM, men bare %2$lld GB av ressurspoolen din på %3$lld GB er ledig. Sett en VM på pause eller slett den, eller oppgrader til Max." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ten rozmiar wymaga %1$lld GB RAM, ale z Twojej puli %3$lld GB wolne jest tylko %2$lld GB. Wstrzymaj lub usuń VM albo przejdź na Max." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este tamanho precisa de %1$lld GB de RAM, mas só %2$lld GB do seu pool de %3$lld GB estão livres. Pause ou exclua uma VM, ou faça upgrade para o Max." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Для этого размера нужно %1$lld ГБ RAM, но в вашем пуле на %3$lld ГБ свободно только %2$lld ГБ. Приостановите или удалите VM либо перейдите на Max." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ขนาดนี้ต้องใช้ RAM %1$lld GB แต่พูล %3$lld GB ของคุณว่างอยู่เพียง %2$lld GB หยุด VM ชั่วคราวหรือลบ VM หรืออัปเกรดเป็น Max" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu boyut %1$lld GB RAM gerektiriyor, ancak %3$lld GB'lık havuzunuzun yalnızca %2$lld GB'ı boş. Bir VM'yi duraklatın veya silin ya da Max'e yükseltin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Для цього розміру потрібно %1$lld ГБ RAM, але у вашому пулі на %3$lld ГБ вільно лише %2$lld ГБ. Призупиніть або видаліть VM чи перейдіть на Max." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "This size needs %1$lld GB RAM, but only %2$lld GB of your %3$lld GB pool is free. Pause or delete a VM, or upgrade to Max." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Größe benötigt %1$lld GB RAM, aber von deinem %3$lld-GB-Pool sind nur %2$lld GB frei. Pausiere oder lösche eine VM, oder führe ein Upgrade auf Max durch." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette taille nécessite %1$lld Go de RAM, mais seuls %2$lld Go de votre réserve de %3$lld Go sont libres. Mettez en pause ou supprimez une VM, ou passez à Max." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يحتاج هذا الحجم إلى %1$lld جيجابايت من الذاكرة، لكن المتاح من مجموعتك البالغة %3$lld جيجابايت هو %2$lld جيجابايت فقط. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه، أو قم بالترقية إلى Max." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este tamaño necesita %1$lld GB de RAM, pero solo quedan libres %2$lld GB de tu grupo de %3$lld GB. Pausa o elimina una VM, o actualiza a Max." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此規格需要 %1$lld GB 記憶體,但你的 %3$lld GB 資源池只剩 %2$lld GB 可用。請暫停或刪除一台虛擬機,或升級至 Max。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此规格需要 %1$lld GB 内存,但你的 %3$lld GB 资源池只剩 %2$lld GB 可用。请暂停或删除一台虚拟机,或升级到 Max。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 크기에는 RAM %1$lld GB가 필요하지만 %3$lld GB 풀 중 %2$lld GB만 남아 있습니다. VM을 일시 중지하거나 삭제하거나 Max로 업그레이드하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このサイズには %1$lld GB RAM が必要ですが、%3$lld GB のプールのうち空きは %2$lld GB だけです。VM を一時停止するか削除する、または Max にアップグレードしてください。" + } + } + } + }, + "cloud.newMachine.pool.doesNotFitMax": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ova veličina treba %1$lld GB RAM-a, ali je od vašeg skupa od %3$lld GB slobodno samo %2$lld GB. Pauzirajte ili izbrišite VM." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelse kræver %1$lld GB RAM, men kun %2$lld GB af din pulje på %3$lld GB er ledig. Sæt en VM på pause eller slet den." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa dimensione richiede %1$lld GB di RAM, ma del tuo pool da %3$lld GB sono liberi solo %2$lld GB. Metti in pausa o elimina una VM." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ទំហំនេះត្រូវការ RAM %1$lld GB ប៉ុន្តែក្នុងបណ្តុំ %3$lld GB របស់អ្នក នៅទំនេរតែ %2$lld GB ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelsen trenger %1$lld GB RAM, men bare %2$lld GB av ressurspoolen din på %3$lld GB er ledig. Sett en VM på pause eller slett den." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ten rozmiar wymaga %1$lld GB RAM, ale z Twojej puli %3$lld GB wolne jest tylko %2$lld GB. Wstrzymaj lub usuń VM." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este tamanho precisa de %1$lld GB de RAM, mas só %2$lld GB do seu pool de %3$lld GB estão livres. Pause ou exclua uma VM." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Для этого размера нужно %1$lld ГБ RAM, но в вашем пуле на %3$lld ГБ свободно только %2$lld ГБ. Приостановите или удалите VM." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ขนาดนี้ต้องใช้ RAM %1$lld GB แต่พูล %3$lld GB ของคุณว่างอยู่เพียง %2$lld GB หยุด VM ชั่วคราวหรือลบ VM" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu boyut %1$lld GB RAM gerektiriyor, ancak %3$lld GB'lık havuzunuzun yalnızca %2$lld GB'ı boş. Bir VM'yi duraklatın veya silin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Для цього розміру потрібно %1$lld ГБ RAM, але у вашому пулі на %3$lld ГБ вільно лише %2$lld ГБ. Призупиніть або видаліть VM." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "This size needs %1$lld GB RAM, but only %2$lld GB of your %3$lld GB pool is free. Pause or delete a VM." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Größe benötigt %1$lld GB RAM, aber von deinem %3$lld-GB-Pool sind nur %2$lld GB frei. Pausiere oder lösche eine VM." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette taille nécessite %1$lld Go de RAM, mais seuls %2$lld Go de votre réserve de %3$lld Go sont libres. Mettez en pause ou supprimez une VM." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يحتاج هذا الحجم إلى %1$lld جيجابايت من الذاكرة، لكن المتاح من مجموعتك البالغة %3$lld جيجابايت هو %2$lld جيجابايت فقط. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este tamaño necesita %1$lld GB de RAM, pero solo quedan libres %2$lld GB de tu grupo de %3$lld GB. Pausa o elimina una VM." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此規格需要 %1$lld GB 記憶體,但你的 %3$lld GB 資源池只剩 %2$lld GB 可用。請暫停或刪除一台虛擬機。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此规格需要 %1$lld GB 内存,但你的 %3$lld GB 资源池只剩 %2$lld GB 可用。请暂停或删除一台虚拟机。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 크기에는 RAM %1$lld GB가 필요하지만 %3$lld GB 풀 중 %2$lld GB만 남아 있습니다. VM을 일시 중지하거나 삭제하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このサイズには %1$lld GB RAM が必要ですが、%3$lld GB のプールのうち空きは %2$lld GB だけです。VM を一時停止するか削除してください。" + } + } + } + }, + "cloud.newMachine.pool.doesNotFitVcpus": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ova veličina treba %1$lld vCPU-a, ali je od vaših %3$lld vCPU-a slobodno samo %2$lld. Pauzirajte ili izbrišite VM, ili pređite na Max." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelse kræver %1$lld vCPU'er, men kun %2$lld af dine %3$lld vCPU'er er ledige. Sæt en VM på pause eller slet den, eller opgrader til Max." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa dimensione richiede %1$lld vCPU, ma delle tue %3$lld vCPU ne sono libere solo %2$lld. Metti in pausa o elimina una VM, oppure passa a Max." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ទំហំនេះត្រូវការ vCPU %1$lld ប៉ុន្តែក្នុងចំណោម vCPU %3$lld របស់អ្នក នៅទំនេរតែ %2$lld ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ ឬដំឡើងទៅ Max។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelsen trenger %1$lld vCPU-er, men bare %2$lld av dine %3$lld vCPU-er er ledige. Sett en VM på pause eller slett den, eller oppgrader til Max." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ten rozmiar wymaga %1$lld vCPU, ale z Twoich %3$lld vCPU wolne jest tylko %2$lld. Wstrzymaj lub usuń VM albo przejdź na Max." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este tamanho precisa de %1$lld vCPUs, mas só %2$lld das suas %3$lld vCPUs estão livres. Pause ou exclua uma VM, ou faça upgrade para o Max." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Для этого размера нужно %1$lld vCPU, но из ваших %3$lld vCPU свободно только %2$lld. Приостановите или удалите VM либо перейдите на Max." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ขนาดนี้ต้องใช้ %1$lld vCPU แต่จาก %3$lld vCPU ของคุณว่างอยู่เพียง %2$lld หยุด VM ชั่วคราวหรือลบ VM หรืออัปเกรดเป็น Max" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu boyut %1$lld vCPU gerektiriyor, ancak %3$lld vCPU'nuzun yalnızca %2$lld tanesi boş. Bir VM'yi duraklatın veya silin ya da Max'e yükseltin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Для цього розміру потрібно %1$lld vCPU, але з ваших %3$lld vCPU вільно лише %2$lld. Призупиніть або видаліть VM чи перейдіть на Max." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "This size needs %1$lld vCPUs, but only %2$lld of your %3$lld vCPUs are free. Pause or delete a VM, or upgrade to Max." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Größe benötigt %1$lld vCPUs, aber von deinen %3$lld vCPUs sind nur %2$lld frei. Pausiere oder lösche eine VM, oder führe ein Upgrade auf Max durch." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette taille nécessite %1$lld vCPU, mais seuls %2$lld de vos %3$lld vCPU sont libres. Mettez en pause ou supprimez une VM, ou passez à Max." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يحتاج هذا الحجم إلى %1$lld من وحدات المعالجة المركزية الافتراضية، لكن المتاح منها %2$lld فقط من أصل %3$lld. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه، أو قم بالترقية إلى Max." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este tamaño necesita %1$lld vCPU, pero solo quedan libres %2$lld de tus %3$lld vCPU. Pausa o elimina una VM, o actualiza a Max." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此規格需要 %1$lld 個虛擬 CPU,但你的 %3$lld 個虛擬 CPU 中只剩 %2$lld 個可用。請暫停或刪除一台虛擬機,或升級至 Max。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此规格需要 %1$lld 个虚拟 CPU,但你的 %3$lld 个虚拟 CPU 中只剩 %2$lld 个可用。请暂停或删除一台虚拟机,或升级到 Max。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 크기에는 vCPU %1$lld개가 필요하지만 vCPU %3$lld개 중 %2$lld개만 남아 있습니다. VM을 일시 중지하거나 삭제하거나 Max로 업그레이드하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このサイズには %1$lld vCPU が必要ですが、%3$lld vCPU のうち空きは %2$lld vCPU だけです。VM を一時停止するか削除する、または Max にアップグレードしてください。" + } + } + } + }, + "cloud.newMachine.pool.doesNotFitVcpusMax": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Ova veličina treba %1$lld vCPU-a, ali je od vaših %3$lld vCPU-a slobodno samo %2$lld. Pauzirajte ili izbrišite VM." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelse kræver %1$lld vCPU'er, men kun %2$lld af dine %3$lld vCPU'er er ledige. Sæt en VM på pause eller slet den." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa dimensione richiede %1$lld vCPU, ma delle tue %3$lld vCPU ne sono libere solo %2$lld. Metti in pausa o elimina una VM." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ទំហំនេះត្រូវការ vCPU %1$lld ប៉ុន្តែក្នុងចំណោម vCPU %3$lld របស់អ្នក នៅទំនេរតែ %2$lld ប៉ុណ្ណោះ។ ផ្អាក ឬលុប VM មួយ។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Denne størrelsen trenger %1$lld vCPU-er, men bare %2$lld av dine %3$lld vCPU-er er ledige. Sett en VM på pause eller slett den." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ten rozmiar wymaga %1$lld vCPU, ale z Twoich %3$lld vCPU wolne jest tylko %2$lld. Wstrzymaj lub usuń VM." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este tamanho precisa de %1$lld vCPUs, mas só %2$lld das suas %3$lld vCPUs estão livres. Pause ou exclua uma VM." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Для этого размера нужно %1$lld vCPU, но из ваших %3$lld vCPU свободно только %2$lld. Приостановите или удалите VM." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ขนาดนี้ต้องใช้ %1$lld vCPU แต่จาก %3$lld vCPU ของคุณว่างอยู่เพียง %2$lld หยุด VM ชั่วคราวหรือลบ VM" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bu boyut %1$lld vCPU gerektiriyor, ancak %3$lld vCPU'nuzun yalnızca %2$lld tanesi boş. Bir VM'yi duraklatın veya silin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Для цього розміру потрібно %1$lld vCPU, але з ваших %3$lld vCPU вільно лише %2$lld. Призупиніть або видаліть VM." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "This size needs %1$lld vCPUs, but only %2$lld of your %3$lld vCPUs are free. Pause or delete a VM." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Größe benötigt %1$lld vCPUs, aber von deinen %3$lld vCPUs sind nur %2$lld frei. Pausiere oder lösche eine VM." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette taille nécessite %1$lld vCPU, mais seuls %2$lld de vos %3$lld vCPU sont libres. Mettez en pause ou supprimez une VM." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يحتاج هذا الحجم إلى %1$lld من وحدات المعالجة المركزية الافتراضية، لكن المتاح منها %2$lld فقط من أصل %3$lld. أوقف جهازًا افتراضيًا مؤقتًا أو احذفه." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este tamaño necesita %1$lld vCPU, pero solo quedan libres %2$lld de tus %3$lld vCPU. Pausa o elimina una VM." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此規格需要 %1$lld 個虛擬 CPU,但你的 %3$lld 個虛擬 CPU 中只剩 %2$lld 個可用。請暫停或刪除一台虛擬機。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此规格需要 %1$lld 个虚拟 CPU,但你的 %3$lld 个虚拟 CPU 中只剩 %2$lld 个可用。请暂停或删除一台虚拟机。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 크기에는 vCPU %1$lld개가 필요하지만 vCPU %3$lld개 중 %2$lld개만 남아 있습니다. VM을 일시 중지하거나 삭제하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このサイズには %1$lld vCPU が必要ですが、%3$lld vCPU のうち空きは %2$lld vCPU だけです。VM を一時停止するか削除してください。" + } + } + } + }, + "machines.new.plan.loading": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Loading your Cloud machine plan…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ تحميل خطة جهاز Cloud…" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Učitavanje plana Cloud mašine…" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indlæser din Cloud-maskineplan…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Cloud-Maschinentarif wird geladen…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cargando el plan de tu máquina Cloud…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chargement du forfait de votre machine Cloud…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Caricamento del piano della macchina Cloud…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud マシンのプランを読み込んでいます…" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កំពុងផ្ទុកគម្រោងម៉ាស៊ីន Cloud របស់អ្នក…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 머신 요금제를 불러오는 중…" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Laster inn Cloud-maskinplanen din…" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wczytywanie planu maszyny Cloud…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Carregando o plano da sua máquina Cloud…" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Загрузка тарифа машины Cloud…" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "กำลังโหลดแผนเครื่อง Cloud ของคุณ…" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Cloud makine planınız yükleniyor…" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Завантаження тарифу машини Cloud…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在加载你的 Cloud 机器方案…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在載入您的 Cloud 機器方案…" + } + } + } + }, + "machines.new.plan.retry": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة المحاولة" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pokušaj ponovo" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Prøv igen" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再試行" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ព្យាយាមម្ដងទៀត" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "재시도" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Prøv igjen" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Ponów" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar Novamente" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Повторить" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ลองใหม่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Tekrar Dene" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Повторити" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重试" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重試" + } + } + } + }, + "machines.new.plan.error": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud plan details are still loading. Retry in a moment." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لا تزال تفاصيل خطة Cloud قيد التحميل. أعد المحاولة بعد قليل." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Detalji Cloud plana se još učitavaju. Pokušajte ponovo za trenutak." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Cloud-planens detaljer indlæses stadig. Prøv igen om et øjeblik." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Details des Cloud-Tarifs werden noch geladen. Versuche es gleich noch einmal." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los detalles del plan de Cloud aún se están cargando. Vuelve a intentarlo en un momento." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les détails du forfait Cloud sont encore en cours de chargement. Réessayez dans un instant." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I dettagli del piano Cloud sono ancora in caricamento. Riprova tra un momento." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud プランの詳細を読み込み中です。しばらくしてから再試行してください。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ព័ត៌មានលម្អិតនៃគម្រោង Cloud នៅតែកំពុងផ្ទុក។ សូមព្យាយាមម្ដងទៀតបន្តិចទៀត។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 요금제 정보를 아직 불러오는 중입니다. 잠시 후 다시 시도하세요." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Detaljene for Cloud-planen lastes fortsatt inn. Prøv igjen om et øyeblikk." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Szczegóły planu Cloud wciąż się wczytują. Spróbuj ponownie za chwilę." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os detalhes do plano Cloud ainda estão carregando. Tente novamente em instantes." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Сведения о тарифе Cloud ещё загружаются. Повторите попытку через мгновение." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รายละเอียดแผน Cloud ยังโหลดอยู่ โปรดลองใหม่อีกครั้งในอีกสักครู่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Cloud planı ayrıntıları hâlâ yükleniyor. Birazdan tekrar deneyin." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Відомості про тариф Cloud ще завантажуються. Повторіть спробу через мить." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "Cloud 方案详情仍在加载。请稍后重试。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "Cloud 方案詳細資料仍在載入。請稍後重試。" + } + } + } + }, + "settings.search.alias.setting.terminal.copy-confirmation": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse تأكيد نسخ حافظة تحديد" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potvrda kopirano međuspremnik odabir" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekræftelse kopieret udklipsholder markering" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse Bestätigung kopiert Zwischenablage Auswahl" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmación copiado portapapeles selección" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmation copié presse-papiers sélection" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse conferma copiato appunti selezione" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 コピー クリップボード 選択" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse បញ្ជាក់ ចម្លង ក្ដារតម្រៀប ជ្រើស" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 확인 복사됨 클립보드 선택" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse bekreftelse kopiert utklippstavle markering" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse potwierdzenie skopiowano schowek zaznaczenie" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse confirmação copiado área de transferência seleção" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse подтверждение скопировано буфер обмена выделение" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse ยืนยัน คัดลอก คลิปบอร์ด เลือก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse onay kopyalandı pano seçim" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse підтвердження скопійовано буфер обміну виділення" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 确认 已复制 剪贴板 选择" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "terminal.showCopyConfirmation copy confirmation copied clipboard feedback indicator toast popup copy on selection select mouse 確認 已複製 剪貼簿 選取" + } + } + } + }, + "settings.terminal.showCopyConfirmation": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار تأكيد النسخ" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prikaži potvrdu kopiranja" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Vis kopieringsbekræftelse" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kopierbestätigung anzeigen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Copy Confirmation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar confirmación de copia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher la confirmation de copie" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra conferma di copia" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コピーの確認を表示" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បង្ហាញការបញ្ជាក់ការចម្លង" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "복사 확인 표시" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Vis kopieringsbekreftelse" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Pokazuj potwierdzenie kopiowania" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar confirmação de cópia" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Показывать подтверждение копирования" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แสดงการยืนยันการคัดลอก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kopyalama onayını göster" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Показувати підтвердження копіювання" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示复制确认" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示複製確認" + } + } + } + }, + "settings.terminal.showCopyConfirmation.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "يعرض «تم النسخ إلى الحافظة» لفترة وجيزة أسفل الطرفية عندما يؤدي تحديد النص إلى نسخه." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Kratko prikazuje „Kopirano u međuspremnik“ na dnu terminala kada se odabrani tekst kopira." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Viser kortvarigt „Kopieret til udklipsholder“ nederst i en terminal, når markering af tekst kopierer den." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt unten im Terminal kurz „In die Zwischenablage kopiert“ an, wenn durch Auswählen von Text kopiert wird." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Briefly shows “Copied to clipboard” at the bottom of a terminal when selecting text copies it." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra brevemente «Copiado al portapapeles» en la parte inferior de una terminal cuando al seleccionar texto se copia." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche brièvement « Copié dans le presse-papiers » en bas d’un terminal lorsque la sélection de texte le copie." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra brevemente «Copiato negli appunti» in fondo a un terminale quando la selezione del testo lo copia." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "テキストを選択してコピーしたときに、ターミナルの下部に「クリップボードにコピーしました」を短く表示します。" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បង្ហាញ «បានចម្លងទៅក្ដារតម្រៀប» មួយភ្លែតនៅខាងក្រោម terminal នៅពេលការជ្រើសអត្ថបទចម្លងវា។" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "텍스트를 선택해 복사하면 터미널 하단에 “클립보드에 복사됨”을 잠시 표시합니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Viser kort «Kopiert til utklippstavlen» nederst i en terminal når markering av tekst kopierer den." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Na chwilę wyświetla „Skopiowano do schowka” u dołu terminala, gdy zaznaczenie tekstu go kopiuje." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostra brevemente “Copiado para a área de transferência” na parte inferior do terminal quando selecionar texto o copia." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Ненадолго показывает «Скопировано в буфер обмена» внизу терминала, когда выделение текста копирует его." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แสดง “คัดลอกไปยังคลิปบอร์ดแล้ว” ชั่วครู่ที่ด้านล่างของเทอร์มินัลเมื่อการเลือกข้อความคัดลอกข้อความนั้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Metin seçimi onu kopyaladığında terminalin altında kısa süreliğine “Panoya kopyalandı” gösterir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ненадовго показує «Скопійовано в буфер обміну» внизу термінала, коли виділення тексту копіює його." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "选择文本并复制时,在终端底部短暂显示“已复制到剪贴板”。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "選取文字並複製時,在終端機底部短暫顯示「已複製到剪貼簿」。" + } + } + } + }, + "pricing.native.compare.resources": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "Resursi Cloud VM-ova" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM-ressourcer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Risorse delle Cloud VM" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ធនធាន Cloud VM" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM-ressurser" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Zasoby Cloud VM" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recursos das Cloud VMs" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Ресурсы Cloud VM" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ทรัพยากร Cloud VM" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM kaynakları" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ресурси Cloud VM" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM resources" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-VM-Ressourcen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressources de VM cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "موارد الجهاز الافتراضي السحابي" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recursos de VM en la nube" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端虛擬機資源" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM 资源" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM 리소스" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud VM のリソース" + } + } + } + }, + "pricing.native.compare.resources.standard": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU-a i 40 GB RAM-a, zajednički" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU'er og 40 GB RAM, delt" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU e 40 GB di RAM, condivisi" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "vCPU 20 និង RAM 40 GB ប្រើរួមគ្នា" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU-er og 40 GB RAM, delt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU i 40 GB RAM, współdzielone" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "20 vCPUs e 40 GB de RAM, compartilhados" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU и 40 ГБ RAM, общие" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU และ RAM 40 GB ใช้ร่วมกัน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU ve 40 GB RAM, paylaşımlı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU і 40 ГБ RAM, спільні" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "20 vCPUs and 40 GB RAM, shared" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "20 vCPUs und 40 GB RAM, gemeinsam genutzt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU et 40 Go de RAM partagés" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "20 وحدة معالجة مركزية افتراضية و40 جيجابايت من الذاكرة مشتركة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU y 40 GB de RAM compartidos" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "共用 20 個虛擬 CPU 與 40 GB 記憶體" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "共享 20 个虚拟 CPU 和 40 GB 内存" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "vCPU 20개와 RAM 40 GB 공유" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU と 40 GB RAM を共有" + } + } + } + }, + "pricing.native.compare.resources.team": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU-a i 40 GB RAM-a po plaćenom mjestu, zajednički za cijeli tim" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU'er og 40 GB RAM pr. betalt plads, delt på tværs af teamet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU e 40 GB di RAM per postazione a pagamento, condivisi da tutto il team" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "vCPU 20 និង RAM 40 GB ក្នុងមួយកៅអីបង់ប្រាក់ ដែលក្រុមទាំងមូលប្រើរួមគ្នា" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU-er og 40 GB RAM per betalt plass, delt på tvers av teamet" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU i 40 GB RAM na płatne miejsce, współdzielone w całym zespole" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "20 vCPUs e 40 GB de RAM por assento pago, compartilhados por toda a equipe" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU и 40 ГБ RAM на оплаченное место, общие для всей команды" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU และ RAM 40 GB ต่อที่นั่งแบบชำระเงิน ใช้ร่วมกันทั้งทีม" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ücretli koltuk başına 20 vCPU ve 40 GB RAM, tüm ekipçe paylaşılır" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU і 40 ГБ RAM на оплачене місце, спільні для всієї команди" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "20 vCPUs and 40 GB RAM per paid seat, shared across the team" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "20 vCPUs und 40 GB RAM pro bezahltem Platz, im ganzen Team gemeinsam genutzt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU et 40 Go de RAM par siège payant, partagés dans toute l’équipe" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "20 وحدة معالجة مركزية افتراضية و40 جيجابايت من الذاكرة لكل مقعد مدفوع، مشتركة على مستوى الفريق" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "20 vCPU y 40 GB de RAM por puesto de pago, compartidos en todo el equipo" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "每個付費席位 20 個虛擬 CPU 與 40 GB 記憶體,由整個團隊共用" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "每个付费席位 20 个虚拟 CPU 和 40 GB 内存,由整个团队共享" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "유료 시트당 vCPU 20개와 RAM 40 GB, 팀 전체가 공유" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "有料シートごとに 20 vCPU と 40 GB RAM(チーム全体で共有)" + } + } + } + }, + "pricing.native.compare.resources.max": { + "extractionState": "manual", + "localizations": { + "bs": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU-a i 160 GB RAM-a, zajednički" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU'er og 160 GB RAM, delt" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU e 160 GB di RAM, condivisi" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "vCPU 80 និង RAM 160 GB ប្រើរួមគ្នា" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU-er og 160 GB RAM, delt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU i 160 GB RAM, współdzielone" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "80 vCPUs e 160 GB de RAM, compartilhados" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU и 160 ГБ RAM, общие" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU และ RAM 160 GB ใช้ร่วมกัน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU ve 160 GB RAM, paylaşımlı" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU і 160 ГБ RAM, спільні" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "80 vCPUs and 160 GB RAM, shared" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "80 vCPUs und 160 GB RAM, gemeinsam genutzt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU et 160 Go de RAM partagés" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "80 وحدة معالجة مركزية افتراضية و160 جيجابايت من الذاكرة مشتركة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU y 160 GB de RAM compartidos" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "共用 80 個虛擬 CPU 與 160 GB 記憶體" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "共享 80 个虚拟 CPU 和 160 GB 内存" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "vCPU 80개와 RAM 160 GB 공유" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "80 vCPU と 160 GB RAM を共有" + } + } + } + }, "sshPane.newTerminalFailed.shortTitle": { "extractionState": "manual", "localizations": { diff --git a/Sources/App/CmuxHelpCommands.swift b/Sources/App/CmuxHelpCommands.swift index 6c794d698cda..5032e7fa31e2 100644 --- a/Sources/App/CmuxHelpCommands.swift +++ b/Sources/App/CmuxHelpCommands.swift @@ -41,6 +41,9 @@ extension cmuxApp { Button(String(localized: "debug.menu.cloudSidebarSpacingLab", defaultValue: "Cloud Sidebar Spacing Lab…")) { AppDelegate.shared?.debugWindowsCoordinator.cloudSidebarDebugLabController.show() } + Button(String(localized: "debug.menu.showCloudWelcome", defaultValue: "Show Cloud Welcome…")) { + AppDelegate.shared?.cloudWelcomeWindowController.present(over: NSApp.mainWindow) + } Button(String(localized: "menu.help.showProWelcomeChecklist", defaultValue: "Show Pro Welcome Checklist…")) { ProWelcomeChecklistPresenter.present() } diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 907a27d8005a..387022bf2d07 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -845,6 +845,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent private(set) var auth: MacAuthComposition? /// Explicit Cloud machine pins and stable fleet order, built by the composition root. private(set) var cloudMachinePinStore: CloudMachinePinStore? + /// "Introducing cmux cloud", shown once from the first main window. + private(set) lazy var cloudWelcomeWindowController = CloudWelcomeWindowController() private(set) lazy var cloudActivationCoordinator = CloudActivationCoordinator(prepare: { try await CmuxTuiSurfaceProviderRegistry.shared.prepareForActivation() }, cleanup: { await CmuxTuiSurfaceProviderRegistry.shared.cancelActivationPreparation() }) var cloudWorkspaceCoordinator: CloudWorkspaceCoordinator? var cloudWorkspaceOperationController: CloudWorkspaceOperationController? @@ -5550,6 +5552,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } attemptStartupSessionRestoreAndSaveIfNeeded(primaryWindow: window) + presentCloudWelcomeIfNeeded(over: window) + } + + /// Once per Mac, after the first main window is up. Tests never see it. + private func presentCloudWelcomeIfNeeded(over window: NSWindow) { + let env = ProcessInfo.processInfo.environment + guard !isRunningUnderXCTestCached, !isRunningUnderXCTest(env), env["CMUX_UI_TEST_MODE"] != "1" else { return } + // Next turn of the main loop, so the window is on screen to place it over. + DispatchQueue.main.async { [weak self, weak window] in + self?.cloudWelcomeWindowController.presentIfNeeded(over: window) + } } #if DEBUG diff --git a/Sources/Auth/HostAccountFlow.swift b/Sources/Auth/HostAccountFlow.swift index 08ef1e0eb5d8..e0e07bdddc8c 100644 --- a/Sources/Auth/HostAccountFlow.swift +++ b/Sources/Auth/HostAccountFlow.swift @@ -23,13 +23,14 @@ final class HostAccountFlow: AccountFlow, AccountSignInFlow { private let featureFlags = CmuxFeatureFlags.shared @ObservationIgnored private var featureFlagsObserver: (any NSObjectProtocol)? private(set) var isProUpgradeAvailable: Bool - private(set) var isProActive = false - private(set) var canManageBilling = false - /// The account whose plan `isProActive` describes, or nil before the - /// billing plan has answered for anyone. Kept here rather than in a view's - /// state so a rebuilt Cloud panel keeps showing Enable Cloud or Upgrade - /// instead of falling back to "Checking your cmux plan…". - private(set) var billingPlanIdentityID: String? + private(set) var billingPlanState = BillingPlanState.unknown + var isProActive: Bool { billingPlanState.isPro } + var canManageBilling: Bool { billingPlanState.canManageBilling } + /// The account whose plan is known, or nil while the plan is unknown. + var billingPlanIdentityID: String? { billingPlanState.accountID } + /// The most recent plan request. Only it may write, so an older request + /// that finishes late cannot overwrite a newer answer. + @ObservationIgnored private var billingPlanRequestID: UUID? /// Whether `isProActive` is a real answer for the signed-in account. var hasLoadedBillingPlan: Bool { guard let billingPlanIdentityID else { return false } @@ -196,9 +197,7 @@ final class HostAccountFlow: AccountFlow, AccountSignInFlow { func signOut() async { await browserSignIn.signOut() - isProActive = false - canManageBilling = false - billingPlanIdentityID = nil + billingPlanState = .unknown } /// Set for the whole switch so sign-in gates show its progress instead of @@ -242,9 +241,7 @@ final class HostAccountFlow: AccountFlow, AccountSignInFlow { /// caller's deadline expires, matching the browser flow contract. func signOut(timeout: TimeInterval) async { await browserSignIn.signOut(timeout: timeout) - isProActive = false - canManageBilling = false - billingPlanIdentityID = nil + billingPlanState = .unknown } func refreshCurrentUser() async { @@ -254,57 +251,42 @@ final class HostAccountFlow: AccountFlow, AccountSignInFlow { } func refreshBillingPlan() async { + _ = await refreshBillingPlanAndReportSuccess() + } + + @discardableResult + func refreshBillingPlanAndReportSuccess() async -> Bool { guard coordinator.currentUser != nil, let identityID = currentIdentity?.id else { - isProActive = false - canManageBilling = false - billingPlanIdentityID = nil - return - } - var request = URLRequest(url: AuthEnvironment.apiBaseURL.appendingPathComponent("api/billing/plan")) - request.httpMethod = "GET" - request.setValue("application/json", forHTTPHeaderField: "Accept") - // URLSession's default is 60 s; a plan check that slow is a failure. - request.timeoutInterval = 15 - - if let tokens = try? await coordinator.currentTokens() { - request.setValue("Bearer \(tokens.accessToken)", forHTTPHeaderField: "Authorization") - request.setValue(tokens.refreshToken, forHTTPHeaderField: "X-Stack-Refresh-Token") + billingPlanState = .unknown + return false } + let requestID = UUID() + billingPlanRequestID = requestID + // Do not project the previous team/account's entitlement while this + // request is in flight. Unknown keeps Cloud enabled until a verified + // response arrives and avoids a false Free/Upgrade state. + billingPlanState = .unknown + let tokens = try? await coordinator.currentTokens() do { - let (data, response) = try await URLSession.shared.data(for: request) - // The account may have changed while the request was in flight. - guard currentIdentity?.id == identityID else { return } - guard let http = response as? HTTPURLResponse, - (200..<300).contains(http.statusCode) else { - forgetBillingPlanUnlessKnown(for: identityID) - return - } - let decoded = try JSONDecoder().decode(BillingPlanResponse.self, from: data) - isProActive = decoded.isPro - canManageBilling = decoded.billingManagement == .stripe - billingPlanIdentityID = identityID + let details = try await BillingPlanClient().fetch( + from: AuthEnvironment.apiBaseURL.appendingPathComponent("api/billing/plan"), + accessToken: tokens?.accessToken, + refreshToken: tokens?.refreshToken + ) + guard currentIdentity?.id == identityID, billingPlanRequestID == requestID else { return false } + billingPlanState = billingPlanState.applyingSuccess( + for: identityID, + isPro: details.isPro, + canManageBilling: details.canManageBilling + ) + return true } catch { // A cancelled request (the panel went away) says nothing about the plan. - if error is CancellationError || (error as? URLError)?.code == .cancelled { return } - guard currentIdentity?.id == identityID else { return } - forgetBillingPlanUnlessKnown(for: identityID) - } - } - - /// A failed check keeps a real answer for the same account, read at the - /// time of the failure (another refresh may have answered meanwhile). - /// With no answer to keep, the plan is unknown rather than "not Pro". - private func forgetBillingPlanUnlessKnown(for identityID: String) { - // Keep a known Pro answer so a transient refresh does not replace the - // upgrade path with Enable Cloud. A cached Free answer is deliberately - // invalidated, since the toggle must remain usable when the refresh - // could not confirm the plan. - guard billingPlanIdentityID == identityID, isProActive else { - isProActive = false - canManageBilling = false - billingPlanIdentityID = nil - return + if error is CancellationError || (error as? URLError)?.code == .cancelled { return false } + guard currentIdentity?.id == identityID, billingPlanRequestID == requestID else { return false } + billingPlanState = billingPlanState.applyingFailure(for: identityID) + return false } } @@ -340,14 +322,3 @@ final class HostAccountFlow: AccountFlow, AccountSignInFlow { ) } } - -private struct BillingPlanResponse: Decodable { - let isPro: Bool - let billingManagement: BillingManagement? -} - -private enum BillingManagement: String, Decodable { - case stripe - case external - case none -} diff --git a/Sources/Cloud/CloudMachinesEnablementView.swift b/Sources/Cloud/CloudMachinesEnablementView.swift index e1e4aadc929f..a0c3d957582d 100644 --- a/Sources/Cloud/CloudMachinesEnablementView.swift +++ b/Sources/Cloud/CloudMachinesEnablementView.swift @@ -1,109 +1,326 @@ import AppKit -import CmuxCloud +import CmuxFoundation import SwiftUI +/// The Cloud tab's first-use outcome. Mirrors +/// ``CloudActivationCoordinator/State`` so the screen renders from plain +/// values (the app maps it in ``CloudMachinesEnablementPanel``; the +/// CloudWelcomeLab builds it directly). +enum CloudMachinesEnablementPhase: Equatable { + enum Failure: Equatable { + case requiresPro + case signInRequired + case serviceUnavailable + } + + case disabled + case enabling + case enabled + case failed(Failure) + case cancelled + case unavailable +} + /// First-use screen for the Cloud tab. It keeps the normal machines panel /// untouched after ``CloudActivationCoordinator/State/enabled`` and gives /// every setup outcome a recoverable action where one exists. /// +/// Before setup starts (Enable for Pro, Upgrade for Free) it introduces Cloud: +/// the app icon with a Cloud badge (a lock for Free), a title for the plan, +/// four reasons, one action and the plan note. Every other outcome is a +/// centered status: symbol, title, one line, its action. +/// /// The screen paints the panel's opaque chrome color. The right sidebar's /// backdrop is a behind-window material (or the translucent window fill when /// backdrops are unified), which the dense machines tree mostly covers, but a /// mostly empty onboarding card would otherwise let other windows show through. +/// +/// It takes plain values and action closures only; the app wires its +/// coordinator and account flow in ``CloudMachinesEnablementPanel``. struct CloudMachinesEnablementView: View { - let coordinator: CloudActivationCoordinator - let accountFlow: HostAccountFlow? + let phase: CloudMachinesEnablementPhase + /// Whether a billing plan request has finished for this account. When it + /// failed, the plan is still unknown and the server decides on enable. let billingPlanLoaded: Bool + /// The account's plan is known (not just "a request finished"). + let isPlanKnown: Bool + let isProActive: Bool + /// Title for ``CloudMachinesEnablementPhase/unavailable`` (the feature + /// flag or managed policy message). + let unavailableMessage: String let chromeBackgroundColor: NSColor + let onEnable: () -> Void + let onCancel: () -> Void + let onRetry: () -> Void + let onUpgrade: () -> Void + /// Nil when there is no account flow to sign in with. + let onSignIn: (() -> Void)? - private static let contentMaxWidth: CGFloat = 320 - private static let actionMaxWidth: CGFloat = 240 + private static let contentMaxWidth: CGFloat = 360 + private static let statusMaxWidth: CGFloat = 280 var body: some View { - ScrollView { - VStack(spacing: 18) { - hero - VStack(spacing: 6) { - Text(title) - .cmuxFont(size: 15, weight: .semibold) - .multilineTextAlignment(.center) - .accessibilityAddTraits(.isHeader) - Text(subtitle) - .cmuxFont(size: 12) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } - .fixedSize(horizontal: false, vertical: true) - if showsBenefits { - CloudMachinesEnablementBenefits() - } - VStack(spacing: 8) { - actionContent + GeometryReader { geometry in + ScrollView { + VStack(spacing: 0) { + content + .padding(.horizontal, 20) + .frame(maxWidth: .infinity) } - .frame(maxWidth: Self.actionMaxWidth) + .frame(minHeight: geometry.size.height, alignment: isIntroduction ? .top : .center) } - .frame(maxWidth: Self.contentMaxWidth) - .padding(.horizontal, 20) - .padding(.vertical, 28) - .frame(maxWidth: .infinity) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(nsColor: chromeBackgroundColor)) .accessibilityIdentifier("CloudMachinesEnablement") } - private var showsBenefits: Bool { - switch coordinator.state { - case .disabled, .cancelled: return true - case .enabling, .enabled, .failed, .unavailable: return false + @ViewBuilder + private var content: some View { + if isIntroduction { + introduction + .frame(maxWidth: Self.contentMaxWidth) + .padding(.vertical, 20) + } else if isCheckingPlan { + planChecking + } else { + status + .frame(maxWidth: Self.statusMaxWidth) + .padding(.vertical, 28) + } + } + + /// Before setup with a loaded (or failed) plan check: the welcome's pitch. + private var isIntroduction: Bool { + phase == .disabled && !isCheckingPlan + } + + private var isCheckingPlan: Bool { + (phase == .disabled || phase == .cancelled) && !billingPlanLoaded && !isPlanKnown + } + + /// Free accounts see Upgrade to Pro in place of Enable Cloud. + private var isProGated: Bool { + isPlanKnown && !isProActive + } + + // MARK: Introduction (Enable or Upgrade) + + private var introduction: some View { + VStack(spacing: 0) { + banner + .padding(.top, 8) + .padding(.bottom, 16) + // A known Free plan is told what it needs; Pro and an unknown plan are invited. + Text(isProGated + ? String(localized: "cloud.enable.intro.requiresPro", defaultValue: "Upgrade to use cmux cloud") + : String(localized: "cloud.enable.intro.title", defaultValue: "Use cmux cloud")) + .cmuxFont(size: 20, weight: .bold) + .multilineTextAlignment(.center) + .accessibilityAddTraits(.isHeader) + Text(String( + localized: "cloud.enable.subtitle", + defaultValue: "Persistent cloud computers that open as regular cmux workspaces." + )) + .cmuxFont(size: 12) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 6) + CloudMachinesEnablementReasons() + .padding(.top, 22) + VStack(spacing: 8) { + introductionAction + Text(planNote) + .cmuxFont(size: 11) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.top, 22) + } + } + + /// The app's icon with a badge: Cloud, or a lock while the plan needs Pro. + /// A computed view, not its own type, so changing it stays a body-only edit. + private var banner: some View { + ZStack(alignment: .bottomTrailing) { + // The dark icon in both appearances (the lab has no asset catalog, so it falls back). + Image(nsImage: NSImage(named: "AppIconDark") ?? NSApp.applicationIconImage) + .resizable() + .frame(width: 60, height: 60) + Image(systemName: isProGated ? "lock.fill" : "cloud.fill") + .cmuxFont(size: isProGated ? 11 : 12, weight: .semibold) + .foregroundStyle(.white) + .frame(width: 24, height: 24) + .background(Circle().fill(Color.accentColor)) + .overlay(Circle().strokeBorder(Color(nsColor: chromeBackgroundColor), lineWidth: 2.5)) + .offset(x: 5, y: 3) + } + .accessibilityHidden(true) + } + + @ViewBuilder + private var introductionAction: some View { + if isProGated { + introButton( + String(localized: "cloud.enable.upgrade", defaultValue: "Upgrade to Pro"), + identifier: "CloudMachinesEnableUpgradeButton", + action: onUpgrade + ) + } else { + introButton( + String(localized: "cloud.enable.action", defaultValue: "Enable Cloud"), + identifier: "CloudMachinesEnableButton", + action: onEnable + ) + } + } + + private func introButton(_ label: String, identifier: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + Text(label) + .frame(minWidth: 160) + } + .controlSize(.large) + .cloudProminentButtonStyle() + .accessibilityIdentifier(identifier) + } + + /// A known Pro plan is told it is covered; Free and an unknown plan (the + /// server decides on enable) are told which plans include Cloud. + private var planNote: String { + if isPlanKnown && isProActive { + return String(localized: "cloud.welcome.note.included", defaultValue: "Included in your plan.") + } + return String(localized: "cloud.welcome.note.available", defaultValue: "Cloud is available on cmux Pro and Max.") + } + + // MARK: Status (every other outcome) + + /// The plan check runs right after the account check; it reads the same. + private var planChecking: some View { + VStack(spacing: 10) { + ProgressView() + .controlSize(.small) + Text(String(localized: "cloud.enable.planChecking", defaultValue: "Checking your cmux plan…")) + .cmuxFont(size: 13) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + } + + private var status: some View { + VStack(spacing: 0) { + statusSymbol + .frame(height: 36) + Text(title) + .cmuxFont(size: 17, weight: .bold) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .accessibilityAddTraits(.isHeader) + .padding(.top, 14) + Text(subtitle) + .cmuxFont(size: 13) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 4) + statusActions + .padding(.top, 18) } } @ViewBuilder - private var hero: some View { - if coordinator.state == .enabling { + private var statusSymbol: some View { + if phase == .enabling { ProgressView() .controlSize(.regular) - .frame(height: 44) .accessibilityLabel(title) } else { - Image(systemName: heroSymbol) - .font(.system(size: 40, weight: .regular)) + Image(systemName: statusSymbolName) + .font(.system(size: 30, weight: .regular)) .symbolRenderingMode(.hierarchical) - .foregroundStyle(heroTint) - .frame(height: 44) + .foregroundStyle(.secondary) .accessibilityHidden(true) } } - private var heroSymbol: String { - if isProGated { return "lock.circle" } - switch coordinator.state { - case .disabled, .cancelled, .enabling, .enabled: return "cloud.fill" - case .failed(.requiresPro): return "lock.circle" - case .failed(.signInRequired): return "person.crop.circle.badge.plus" + private var statusSymbolName: String { + switch phase { + case .disabled, .cancelled, .enabling, .enabled, .failed(.requiresPro): return "cloud" + case .failed(.signInRequired): return "person.crop.circle" case .failed(.serviceUnavailable): return "exclamationmark.icloud" case .unavailable: return "icloud.slash" } } - private var heroTint: Color { - if isProGated { return .secondary } - switch coordinator.state { - case .disabled, .cancelled, .enabling, .enabled, .failed(.signInRequired): return .accentColor - case .failed(.requiresPro): return .secondary - case .failed(.serviceUnavailable): return .orange - case .unavailable: return .secondary + @ViewBuilder + private var statusActions: some View { + switch phase { + case .disabled, .cancelled: + if isProGated { + actionButton( + String(localized: "cloud.enable.upgrade", defaultValue: "Upgrade to Pro"), + prominent: true, + identifier: "CloudMachinesEnableUpgradeButton", + action: onUpgrade + ) + } else { + actionButton( + String(localized: "cloud.enable.action", defaultValue: "Enable Cloud"), + prominent: true, + identifier: "CloudMachinesEnableButton", + action: onEnable + ) + } + case .enabling: + actionButton( + String(localized: "cloud.enable.cancel", defaultValue: "Cancel"), + prominent: false, + identifier: "CloudMachinesEnableCancelButton", + action: onCancel + ) + case .failed(.requiresPro): + actionPair { + retryButton(prominent: false) + actionButton( + String(localized: "cloud.enable.upgrade", defaultValue: "Upgrade to Pro"), + prominent: true, + identifier: "CloudMachinesEnableUpgradeButton", + action: onUpgrade + ) + } + case .failed(.signInRequired): + if let onSignIn { + actionPair { + retryButton(prominent: false) + actionButton( + String(localized: "cloud.enable.signIn.action", defaultValue: "Sign In"), + prominent: true, + identifier: "CloudMachinesEnableSignInButton", + action: onSignIn + ) + } + } else { + retryButton(prominent: true) + } + case .failed(.serviceUnavailable): + retryButton(prominent: true) + case .enabled, .unavailable: + EmptyView() } } + // MARK: Copy + private var title: String { if isProGated { return String(localized: "cloud.enable.requiresPro.title", defaultValue: "Cloud Machines require cmux Pro") } - switch coordinator.state { + switch phase { case .disabled, .cancelled, .enabled: - return String(localized: "cloud.enable.title", defaultValue: "Use Cloud Machines") + return String(localized: "cloud.enable.title.enable", defaultValue: "Enable Cloud Machines") case .enabling: return String(localized: "cloud.enable.loading.title", defaultValue: "Setting up Cloud Machines…") case .failed(.requiresPro): @@ -113,30 +330,31 @@ struct CloudMachinesEnablementView: View { case .failed(.serviceUnavailable): return String(localized: "cloud.enable.failed.title", defaultValue: "Cloud setup is temporarily unavailable") case .unavailable: - return CloudMachinesFeature.disabledMessage + return unavailableMessage } } + /// The status line. The introduction always uses the product line instead. private var subtitle: String { if isProGated { return String(localized: "cloud.enable.requiresPro.subtitle", defaultValue: "This account’s plan does not include Cloud machine access.") } - switch coordinator.state { + switch phase { case .disabled, .enabled: return String( localized: "cloud.enable.subtitle", defaultValue: "Persistent cloud computers that open as regular cmux workspaces." ) - case .enabling: - return String( - localized: "cloud.enable.loading.subtitle", - defaultValue: "cmux is preparing the shared Cloud connection. This can take a moment." - ) case .cancelled: return String( localized: "cloud.enable.cancelled.subtitle", defaultValue: "Cloud was not enabled. You can start setup again whenever you are ready." ) + case .enabling: + return String( + localized: "cloud.enable.loading.subtitle", + defaultValue: "cmux is preparing the shared Cloud connection. This can take a moment." + ) case .failed(.requiresPro): return String( localized: "cloud.enable.requiresPro.subtitle", @@ -160,87 +378,29 @@ struct CloudMachinesEnablementView: View { } } - @ViewBuilder - private var actionContent: some View { - switch coordinator.state { - case .disabled, .cancelled: - if !billingPlanLoaded { - Text(String(localized: "cloud.enable.planChecking", defaultValue: "Checking your cmux plan…")) - .cmuxFont(size: 12) - .foregroundStyle(.secondary) - } else if isProGated { - actionButton( - String(localized: "cloud.enable.upgrade", defaultValue: "Upgrade to Pro"), - prominent: true, - identifier: "CloudMachinesEnableUpgradeButton" - ) { - ProUpgradePresenter.present(source: .machinesPanelRequiresPro) - } - } else { - Button { - coordinator.enable() - } label: { - Text(String(localized: "cloud.enable.action", defaultValue: "Enable Cloud")) - .frame(maxWidth: .infinity) - } - .cloudProminentButtonStyle() - .controlSize(.regular) - .accessibilityIdentifier("CloudMachinesEnableButton") - Text(String(localized: "cloud.enable.planNote", defaultValue: "Requires a cmux Pro plan.")) - .cmuxFont(size: 11) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - case .enabling: - actionButton( - String(localized: "cloud.enable.cancel", defaultValue: "Cancel"), - prominent: false, - identifier: "CloudMachinesEnableCancelButton" - ) { - coordinator.cancel() - } - case .failed(.requiresPro): - actionButton( - String(localized: "cloud.enable.upgrade", defaultValue: "Upgrade to Pro"), - prominent: true, - identifier: "CloudMachinesEnableUpgradeButton" - ) { - ProUpgradePresenter.present(source: .machinesPanelRequiresPro) - } - retryButton(prominent: false) - case .failed(.signInRequired): - if let accountFlow { - actionButton( - String(localized: "cloud.enable.signIn.action", defaultValue: "Sign In"), - prominent: true, - identifier: "CloudMachinesEnableSignInButton" - ) { - accountFlow.startSignIn() - } - } - retryButton(prominent: accountFlow == nil) - case .failed(.serviceUnavailable): - retryButton(prominent: true) - case .enabled, .unavailable: - EmptyView() - } - } - - private var isProGated: Bool { - billingPlanLoaded && accountFlow?.isProActive != true - } + // MARK: Buttons private func retryButton(prominent: Bool) -> some View { actionButton( String(localized: "machines.unavailable.retry", defaultValue: "Retry"), prominent: prominent, - identifier: "CloudMachinesEnableRetryButton" - ) { - coordinator.retry() + identifier: "CloudMachinesEnableRetryButton", + action: onRetry + ) + } + + /// Two actions side by side, secondary first like a dialog; stacked when a + /// translation does not fit the sidebar. + private func actionPair(@ViewBuilder _ buttons: () -> Content) -> some View { + let buttons = buttons() + return ViewThatFits(in: .horizontal) { + HStack(spacing: 8) { buttons } + .fixedSize() + VStack(spacing: 8) { buttons } } } + /// Large buttons, the primary one blue even in an inactive window. @ViewBuilder private func actionButton( _ label: String, @@ -249,9 +409,12 @@ struct CloudMachinesEnablementView: View { action: @escaping () -> Void ) -> some View { let button = Button(action: action) { - Text(label).frame(maxWidth: .infinity) + Text(label) + .padding(.horizontal, 6) } - .controlSize(.regular) + // No shape here: the right sidebar gives every button its 6pt radius + // (rightSidebarButtonBorderShape on the panel). + .controlSize(.large) .accessibilityIdentifier(identifier) if prominent { button.cloudProminentButtonStyle() @@ -273,53 +436,64 @@ extension View { } } -/// Three short reasons to enable Cloud, shown before setup starts. Each line -/// restates a shipped capability from the Cloud overview docs. -private struct CloudMachinesEnablementBenefits: View { +/// Four reasons to enable Cloud: an accent symbol, a short title and one line. +/// Each restates a shipped capability from the Cloud overview docs. +private struct CloudMachinesEnablementReasons: View { var body: some View { - VStack(alignment: .leading, spacing: 10) { + VStack(alignment: .leading, spacing: 14) { row( symbol: "terminal", - text: String( + title: String(localized: "cloud.enable.reason.away", defaultValue: "Runs while you’re away"), + detail: String( localized: "cloud.enable.benefit.agents", defaultValue: "Agents and terminals keep running after you close your laptop." ) ) row( symbol: "externaldrive", - text: String( + title: String(localized: "cloud.enable.reason.setUp", defaultValue: "Stays set up"), + detail: String( localized: "cloud.enable.benefit.files", defaultValue: "Files and installed tools stay on the machine between sessions." ) ) row( symbol: "laptopcomputer", - text: String( + title: String(localized: "cloud.enable.reason.anyMac", defaultValue: "Opens on any Mac"), + detail: String( localized: "cloud.enable.benefit.reattach", defaultValue: "Pick up where you left off from any Mac you sign in on." ) ) + row( + symbol: "person.2", + title: String(localized: "cloud.enable.reason.team", defaultValue: "Shared with your team"), + detail: String( + localized: "cloud.enable.reason.team.detail", + defaultValue: "Invite teammates, and share any port with a private link." + ) + ) } - .padding(12) .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(Color.primary.opacity(0.05)) - ) .accessibilityElement(children: .contain) } - private func row(symbol: String, text: String) -> some View { - HStack(alignment: .firstTextBaseline, spacing: 10) { + private func row(symbol: String, title: String, detail: String) -> some View { + HStack(alignment: .top, spacing: 10) { Image(systemName: symbol) - .cmuxFont(size: 13) + .cmuxFont(size: 16) .foregroundStyle(Color.accentColor) - .frame(width: 18) + .frame(width: 24) + .padding(.top, 1) .accessibilityHidden(true) - Text(text) - .cmuxFont(size: 12) - .foregroundStyle(.primary) - .fixedSize(horizontal: false, vertical: true) + VStack(alignment: .leading, spacing: 2) { + Text(title) + .cmuxFont(size: 13, weight: .semibold) + Text(detail) + .cmuxFont(size: 12) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } } .accessibilityElement(children: .combine) } diff --git a/Sources/Cloud/CloudWelcomeHero.swift b/Sources/Cloud/CloudWelcomeHero.swift new file mode 100644 index 000000000000..31bce60f46a9 --- /dev/null +++ b/Sources/Cloud/CloudWelcomeHero.swift @@ -0,0 +1,131 @@ +import AppKit +import CmuxFoundation +import SwiftUI + +/// The welcome window's hero: the Cloud tree as the right sidebar shows it +/// mid-work (one machine open on its workspace and two terminals, Terminals +/// selected), floating as a glass panel. The names are illustration, not data, +/// so they are verbatim. +struct CloudWelcomeHero: View { + var body: some View { + machineList + .frame(width: 340) + .cloudWelcomeGlassPanel(cornerRadius: 14) + .padding(.top, 18) + .padding(.bottom, 22) + .frame(maxWidth: .infinity) + .accessibilityHidden(true) + } + + private var machineList: some View { + VStack(alignment: .leading, spacing: 0) { + header + row(symbol: "chevron.down", name: "gentle-blue-meerkat") + row(symbol: "folder.fill", name: "workspace-1", isChild: true, isOpen: true) + leafRow(symbol: "terminal", name: "codex", depth: 2) + leafRow(symbol: "terminal", name: "npm run dev", depth: 2) + HStack(spacing: 2) { + tab(String(localized: "cloudTree.group.ports", defaultValue: "Ports"), count: 2) + tab(String(localized: "cloudTree.group.terminals", defaultValue: "Terminals"), count: 2, selected: true) + tab(String(localized: "cloudTree.group.displays", defaultValue: "Displays"), count: 1) + tab(String(localized: "cloudTree.group.resources", defaultValue: "Resources"), count: nil) + } + .padding(EdgeInsets(top: 3, leading: 18, bottom: 3, trailing: 0)) + row(symbol: "chevron.right", name: "lucky-ochre-mongoose") + row(symbol: "chevron.right", name: "loyal-ruby-koi") + } + .padding(EdgeInsets(top: 8, leading: 12, bottom: 6, trailing: 12)) + } + + private var header: some View { + HStack(spacing: 6) { + Image(systemName: "cloud") + .cmuxFont(size: 11) + Text(String(localized: "cloudTree.group.cloudMachines", defaultValue: "Cloud Machines")) + .cmuxFont(size: 12) + Text(verbatim: "3/5") + .cmuxFont(size: 12) + .monospacedDigit() + .foregroundStyle(.tertiary) + .padding(.leading, -1) + Spacer(minLength: 0) + } + .foregroundStyle(.secondary) + .padding(.bottom, 4) + } + + private func row(symbol: String, name: String, isChild: Bool = false, isOpen: Bool = false) -> some View { + HStack(spacing: 6) { + if isChild { + // A workspace row: disclosure chevron, then its folder. + Image(systemName: isOpen ? "chevron.down" : "chevron.right") + .cmuxFont(size: 9, weight: .semibold) + .foregroundStyle(.secondary) + .frame(width: 14) + } + Image(systemName: symbol) + .cmuxFont(size: isChild ? 11 : 9, weight: isChild ? .regular : .semibold) + .foregroundStyle(.secondary) + .frame(width: 14) + Text(verbatim: name) + .cmuxFont(size: 13) + } + .padding(.leading, isChild ? 18 : 0) + .frame(height: 24) + } + + private func leafRow(symbol: String, name: String, depth: Int) -> some View { + HStack(spacing: 6) { + Image(systemName: symbol) + .cmuxFont(size: 11) + .foregroundStyle(.secondary) + .frame(width: 14) + Text(verbatim: name) + .cmuxFont(size: 13) + } + .padding(.leading, CGFloat(depth) * 18 + 20) + .frame(height: 24) + } + + private func tab(_ label: String, count: Int?, selected: Bool = false) -> some View { + HStack(spacing: 4) { + Text(label) + .lineLimit(1) + .fixedSize() + .foregroundStyle(selected ? .primary : .secondary) + if let count { + Text(verbatim: "\(count)") + .monospacedDigit() + .foregroundStyle(.tertiary) + } + } + .cmuxFont(size: 11) + .padding(.horizontal, 6) + .frame(height: 20) + .background( + RoundedRectangle(cornerRadius: 5, style: .continuous) + .fill(Color.primary.opacity(selected ? 0.08 : 0)) + ) + } +} + +private extension View { + /// Liquid Glass on macOS 26; a quiet translucent fill with a hairline before it. + @ViewBuilder + func cloudWelcomeGlassPanel(cornerRadius: CGFloat) -> some View { + let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + #if compiler(>=6.2) + if #available(macOS 26.0, *) { + self.glassEffect(.regular, in: shape) + } else { + self + .background(shape.fill(Color.primary.opacity(0.06))) + .overlay(shape.strokeBorder(Color.primary.opacity(0.14), lineWidth: 0.5)) + } + #else + self + .background(shape.fill(Color.primary.opacity(0.06))) + .overlay(shape.strokeBorder(Color.primary.opacity(0.14), lineWidth: 0.5)) + #endif + } +} diff --git a/Sources/Cloud/CloudWelcomeView.swift b/Sources/Cloud/CloudWelcomeView.swift new file mode 100644 index 000000000000..4689e4e52294 --- /dev/null +++ b/Sources/Cloud/CloudWelcomeView.swift @@ -0,0 +1,234 @@ +import AppKit +import CmuxFoundation +import SwiftUI + +/// What the welcome window's one prominent button does for this account. +enum CloudWelcomeNextStep: Equatable { + case signIn + case upgrade + case enable + /// The plan is not known yet (or could not be loaded): open the Cloud tab, + /// whose own gate decides between Upgrade and Enable. + case openCloud + + static func resolve(isAuthenticated: Bool, isPlanKnown: Bool, isPro: Bool) -> CloudWelcomeNextStep { + guard isAuthenticated else { return .signIn } + guard isPlanKnown else { return .openCloud } + return isPro ? .enable : .upgrade + } +} + +/// "Introducing cmux cloud": shown once on launch (new users, and existing +/// users after the update that ships it). A glass window with the title, a +/// Cloud tree built from the sidebar's own rows, and a solid panel with the +/// reasons and the next step. +/// +/// Takes plain values (no app objects) so the same view renders in the app and +/// in a standalone lab; ``CloudWelcomeWindowController`` feeds it the account. +struct CloudWelcomeView: View { + let nextStep: CloudWelcomeNextStep + let onNotNow: () -> Void + let onNext: (CloudWelcomeNextStep) -> Void + + static let windowWidth: CGFloat = 580 + private static let panelCornerRadius: CGFloat = 16 + + var body: some View { + VStack(spacing: 0) { + title + .frame(maxWidth: .infinity) + .padding(.top, 26) + .padding(.bottom, 2) + CloudWelcomeHero() + panel + .padding(.horizontal, 10) + .padding(.bottom, 10) + } + .frame(width: Self.windowWidth) + .background(windowBackground) + .accessibilityIdentifier("CloudWelcomeWindow") + } + + /// On macOS 26 the window hosts this view inside an NSGlassEffectView (the + /// controller does that), so the glass is the window itself: it fills the + /// window to its own corners and never lenses this content. SwiftUI's + /// .glassEffect painted behind the content did both (a second rim inside the + /// window edge, and a ghost of the title). Earlier macOS gets the window material. + @ViewBuilder + private var windowBackground: some View { + #if compiler(>=6.2) + if #available(macOS 26.0, *) { + Color.clear + } else { + CloudWelcomeVisualEffect() + .ignoresSafeArea() + } + #else + CloudWelcomeVisualEffect() + .ignoresSafeArea() + #endif + } + + private var panel: some View { + VStack(alignment: .leading, spacing: 0) { + // The title is above the hero, so the subtitle leads the panel. + Text(String( + localized: "cloud.enable.subtitle", + defaultValue: "Persistent cloud computers that open as regular cmux workspaces." + )) + .cmuxFont(size: 15, weight: .medium) + VStack(alignment: .leading, spacing: 12) { + CloudWelcomeReasonRow( + symbol: "terminal", + text: String( + localized: "cloud.enable.benefit.agents", + defaultValue: "Agents and terminals keep running after you close your laptop." + ) + ) + CloudWelcomeReasonRow( + symbol: "laptopcomputer", + text: String( + localized: "cloud.enable.benefit.reattach", + defaultValue: "Pick up where you left off from any Mac you sign in on." + ) + ) + CloudWelcomeReasonRow( + symbol: "person.2", + text: String( + localized: "cloud.enable.reason.team.detail", + defaultValue: "Invite teammates, and share any port with a private link." + ) + ) + } + .padding(.top, 16) + footer + .padding(.top, 20) + } + .padding(EdgeInsets(top: 22, leading: 24, bottom: 16, trailing: 24)) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: Self.panelCornerRadius, style: .continuous) + .fill(Color(nsColor: .windowBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: Self.panelCornerRadius, style: .continuous) + .strokeBorder(Color(nsColor: .separatorColor), lineWidth: 0.5) + ) + } + + /// "Introducing" over "cmux cloud" and a "new" badge. The name is the + /// product, not a sentence, so it is the same in every language. + private var title: some View { + VStack(spacing: 2) { + Text(String(localized: "cloud.welcome.title.eyebrow", defaultValue: "Introducing")) + .cmuxFont(size: 13, weight: .medium) + .foregroundStyle(.secondary) + HStack(alignment: .center, spacing: 10) { + Text(verbatim: "cmux cloud") + .cmuxFont(size: 30, weight: .bold) + Text(String(localized: "cloud.welcome.newBadge", defaultValue: "New").lowercased(with: .current)) + .cmuxFont(size: 12, weight: .medium) + .foregroundStyle(Color.accentColor) + .padding(.horizontal, 8) + .frame(height: 20) + .background(Capsule(style: .continuous).fill(Color.accentColor.opacity(0.18))) + } + } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isHeader) + } + + private var footer: some View { + HStack(alignment: .center, spacing: 8) { + VStack(alignment: .leading, spacing: 1) { + ForEach(noteLines, id: \.self) { line in + Text(line) + } + } + .cmuxFont(size: 11) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + Spacer(minLength: 12) + Button(action: onNotNow) { + Text(String(localized: "common.notNow", defaultValue: "Not Now")) + .padding(.horizontal, 6) + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .keyboardShortcut(.cancelAction) + .accessibilityIdentifier("CloudWelcomeNotNowButton") + Button { + onNext(nextStep) + } label: { + Text(primaryLabel) + .padding(.horizontal, 6) + } + .buttonStyle(.borderedProminent) + .buttonBorderShape(.capsule) + .controlSize(.large) + .environment(\.controlActiveState, .key) + // No Return shortcut: the window appears on its own at launch, and a + // Return meant for the terminal must not enable Cloud or open pricing. + .accessibilityIdentifier("CloudWelcomePrimaryButton") + } + } + + private var primaryLabel: String { + switch nextStep { + case .signIn: + return String(localized: "cloud.enable.signIn.action", defaultValue: "Sign In") + case .upgrade: + return String(localized: "cloud.enable.upgrade", defaultValue: "Upgrade to Pro") + case .enable: + return String(localized: "cloud.enable.action", defaultValue: "Enable Cloud") + case .openCloud: + return String(localized: "cloud.welcome.setUp", defaultValue: "Set Up Cloud") + } + } + + private var noteLines: [String] { + let available = String( + localized: "cloud.welcome.note.available", + defaultValue: "Cloud is available on cmux Pro and Max." + ) + switch nextStep { + case .enable: + return [String(localized: "cloud.welcome.note.included", defaultValue: "Included in your plan.")] + case .signIn, .upgrade, .openCloud: + return [available] + } + } +} + +/// The window material before macOS 26 (what the About window uses). +private struct CloudWelcomeVisualEffect: NSViewRepresentable { + func makeNSView(context: Context) -> NSVisualEffectView { + let view = NSVisualEffectView() + view.material = .underWindowBackground + view.blendingMode = .behindWindow + view.state = .active + return view + } + + func updateNSView(_ nsView: NSVisualEffectView, context: Context) {} +} + +private struct CloudWelcomeReasonRow: View { + let symbol: String + let text: String + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 10) { + Image(systemName: symbol) + .cmuxFont(size: 14) + .foregroundStyle(.secondary) + .frame(width: 20) + .accessibilityHidden(true) + Text(text) + .cmuxFont(size: 13) + .fixedSize(horizontal: false, vertical: true) + } + .accessibilityElement(children: .combine) + } +} diff --git a/Sources/Cloud/CloudWelcomeWindowController.swift b/Sources/Cloud/CloudWelcomeWindowController.swift new file mode 100644 index 000000000000..2ac2fd015c68 --- /dev/null +++ b/Sources/Cloud/CloudWelcomeWindowController.swift @@ -0,0 +1,162 @@ +import AppKit +import CmuxCloud +import SwiftUI + +/// Shows "Introducing cmux Cloud" once: on first launch, and for existing users +/// on the first launch after the update that ships it (they have no seen key +/// yet either). Debug builds can reopen it from Help. +@MainActor +final class CloudWelcomeWindowController: NSObject, NSWindowDelegate { + static let seenDefaultsKey = "cmux.cloud.welcome.seen" + + private var window: NSWindow? + /// Launch presentation is considered once, at the first main window. A + /// window opened later (Cmd+N an hour in) must not pop the welcome up just + /// because remote flags arrived since; an unseen welcome waits for next launch. + private var didConsiderLaunchPresentation = false + + /// Cloud has to be offered on this Mac and still be off; `seen` makes it once. + nonisolated static func shouldPresentAutomatically( + seen: Bool, + cloudAvailable: Bool, + cloudEnabled: Bool + ) -> Bool { + !seen && cloudAvailable && !cloudEnabled + } + + /// Presents at launch when it applies, and marks it seen on the way so a + /// quit or crash while it is open does not show it again. + func presentIfNeeded(over parent: NSWindow?, defaults: UserDefaults = .standard) { + guard !didConsiderLaunchPresentation else { return } + didConsiderLaunchPresentation = true + guard Self.shouldPresentAutomatically( + seen: defaults.bool(forKey: Self.seenDefaultsKey), + cloudAvailable: CloudMachinesFeature.isAvailable, + cloudEnabled: CloudMachinesFeature.isEnabled + ) else { return } + defaults.set(true, forKey: Self.seenDefaultsKey) + present(over: parent) + } + + func present(over parent: NSWindow?) { + window?.close() + let window = makeWindow() + self.window = window + position(window, over: parent) + NSApp.activate(ignoringOtherApps: true) + window.makeKeyAndOrderFront(nil) + } + + private func makeWindow() -> NSWindow { + let rootView = CloudWelcomeAccountView( + accountFlow: AppDelegate.shared?.auth?.accountFlow, + onNotNow: { [weak self] in self?.dismiss() }, + onNext: { [weak self] step in self?.perform(step) } + ) + let hosting = NSHostingView(rootView: rootView) + // The content clears the traffic lights with its own top padding, so it + // needs no titlebar safe area. Tracking it also loops in the app: the + // hosting view keeps invalidating its safe area and constraints until + // AppKit aborts (too many Update Constraints passes, 2026-10-04). + hosting.safeAreaRegions = [] + let size = hosting.fittingSize + let window = NSWindow( + contentRect: NSRect(origin: .zero, size: NSSize(width: CloudWelcomeView.windowWidth, height: size.height)), + styleMask: [.titled, .closable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.identifier = NSUserInterfaceItemIdentifier("cmux.cloud.welcome") + window.title = String(localized: "cloud.welcome.title", defaultValue: "Introducing cmux cloud") + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.isMovableByWindowBackground = true + window.isReleasedWhenClosed = false + // The glass in CloudWelcomeView is the window's background. + window.isOpaque = false + window.backgroundColor = .clear + window.standardWindowButton(.miniaturizeButton)?.isEnabled = false + window.standardWindowButton(.zoomButton)?.isEnabled = false + window.contentView = hosting + #if compiler(>=6.2) + if #available(macOS 26.0, *) { + // The window is the glass: content goes inside it, edge to edge, and the + // window's own frame rounds the corners. + let glass = NSGlassEffectView() + glass.style = .regular + glass.contentView = hosting + window.contentView = glass + } + #endif + window.delegate = self + return window + } + + /// A third of the way down the parent window, centered across it, like + /// NSWindow.center() places windows on the screen. + private func position(_ window: NSWindow, over parent: NSWindow?) { + guard let parent, parent.isVisible else { + window.center() + return + } + let size = window.frame.size + let frame = parent.frame + let origin = NSPoint( + x: frame.midX - size.width / 2, + y: frame.maxY - (frame.height - size.height) / 3 - size.height + ) + window.setFrameOrigin(origin) + } + + private func perform(_ step: CloudWelcomeNextStep) { + dismiss() + let app = AppDelegate.shared + switch step { + case .signIn: + _ = app?.focusRightSidebarInActiveMainWindow(mode: .machines) + app?.auth?.accountFlow.startSignIn() + case .upgrade: + ProUpgradePresenter.present(source: .cloudWelcome) + case .enable: + _ = app?.focusRightSidebarInActiveMainWindow(mode: .machines) + app?.cloudActivationCoordinator.enable() + case .openCloud: + _ = app?.focusRightSidebarInActiveMainWindow(mode: .machines) + } + } + + func dismiss() { + window?.close() + window = nil + } + + func windowWillClose(_ notification: Notification) { + window = nil + } +} + +/// Feeds the account's sign-in and plan into ``CloudWelcomeView`` and asks for +/// the plan once on show, so the view itself stays free of app objects. +private struct CloudWelcomeAccountView: View { + let accountFlow: HostAccountFlow? + let onNotNow: () -> Void + let onNext: (CloudWelcomeNextStep) -> Void + + var body: some View { + CloudWelcomeView( + nextStep: CloudWelcomeNextStep.resolve( + isAuthenticated: accountFlow?.isAuthenticated == true, + isPlanKnown: accountFlow?.hasLoadedBillingPlan == true, + isPro: accountFlow?.isProActive == true + ), + onNotNow: onNotNow, + onNext: onNext + ) + .task { + // The plan decides between Upgrade and Enable; ask once on show. + if accountFlow?.isAuthenticated == true { + await accountFlow?.refreshBillingPlan() + } + } + } +} diff --git a/Sources/Cloud/MachinesPanelView+Activation.swift b/Sources/Cloud/MachinesPanelView+Activation.swift index 4617a8ac473b..24520ccae80e 100644 --- a/Sources/Cloud/MachinesPanelView+Activation.swift +++ b/Sources/Cloud/MachinesPanelView+Activation.swift @@ -1,3 +1,5 @@ +import AppKit +import CmuxCloud import SwiftUI extension MachinesPanelView { @@ -31,7 +33,7 @@ extension MachinesPanelView { case .signedOut: authGate case .signedIn: - CloudMachinesEnablementView( + CloudMachinesEnablementPanel( coordinator: activationCoordinator, accountFlow: accountFlow, billingPlanLoaded: billingPlanLoaded, @@ -42,3 +44,49 @@ extension MachinesPanelView { } } + +/// Wires ``CloudMachinesEnablementView`` to the app: reads the coordinator and +/// account flow in its own body (so their changes redraw only this screen) +/// and routes each action to its service. +struct CloudMachinesEnablementPanel: View { + let coordinator: CloudActivationCoordinator + let accountFlow: HostAccountFlow? + let billingPlanLoaded: Bool + let chromeBackgroundColor: NSColor + + var body: some View { + CloudMachinesEnablementView( + phase: CloudMachinesEnablementPhase(coordinator.state), + billingPlanLoaded: billingPlanLoaded, + isPlanKnown: accountFlow?.hasLoadedBillingPlan == true, + isProActive: accountFlow?.isProActive == true, + unavailableMessage: CloudMachinesFeature.disabledMessage, + chromeBackgroundColor: chromeBackgroundColor, + onEnable: { coordinator.enable() }, + onCancel: { coordinator.cancel() }, + onRetry: { coordinator.retry() }, + onUpgrade: { ProUpgradePresenter.present(source: .machinesPanelRequiresPro) }, + onSignIn: signIn + ) + } + + private var signIn: (() -> Void)? { + guard let accountFlow else { return nil } + return { accountFlow.startSignIn() } + } +} + +private extension CloudMachinesEnablementPhase { + init(_ state: CloudActivationCoordinator.State) { + switch state { + case .disabled: self = .disabled + case .enabling: self = .enabling + case .enabled: self = .enabled + case .failed(.requiresPro): self = .failed(.requiresPro) + case .failed(.signInRequired): self = .failed(.signInRequired) + case .failed(.serviceUnavailable): self = .failed(.serviceUnavailable) + case .cancelled: self = .cancelled + case .unavailable: self = .unavailable + } + } +} diff --git a/Sources/Cloud/MachinesPanelView.swift b/Sources/Cloud/MachinesPanelView.swift index 13badab08e52..4af0400886f2 100644 --- a/Sources/Cloud/MachinesPanelView.swift +++ b/Sources/Cloud/MachinesPanelView.swift @@ -142,7 +142,13 @@ struct MachinesPanelView: View { billingPlanLoaded = false return } - billingPlanLoaded = false + // The panel is rebuilt whenever the sidebar switches modes. Start + // from the account's last answer so Enable Cloud / Upgrade stays + // on screen while it refreshes, instead of flashing "Checking…". + billingPlanLoaded = accountFlow.hasLoadedBillingPlan + // The enable action is available while the entitlement is unknown. + // The response itself is the synchronization event. + billingPlanLoaded = true await accountFlow.refreshBillingPlan() guard !Task.isCancelled, accountFlow.isAuthenticated, diff --git a/Sources/HostSettingsActions+Cloud.swift b/Sources/HostSettingsActions+Cloud.swift index 1faeefd04c2f..45514f5c40a8 100644 --- a/Sources/HostSettingsActions+Cloud.swift +++ b/Sources/HostSettingsActions+Cloud.swift @@ -70,12 +70,24 @@ extension HostSettingsActions { } var cloudMachinesAccountID: String? { - AppDelegate.shared?.auth?.accountFlow.currentIdentity?.id + guard let flow = AppDelegate.shared?.auth?.accountFlow, + let accountID = flow.currentIdentity?.id else { return nil } + // Include the active team so a team switch reruns entitlement lookup + // even though the signed-in account remains unchanged. + let teamID = flow.confirmedTeamID ?? "personal" + return "\(accountID):\(teamID)" } func cloudMachinesPlanIncludesCloud() async -> Bool? { - guard let flow = AppDelegate.shared?.auth?.accountFlow, flow.isAuthenticated else { return nil } - await flow.refreshBillingPlan() + guard let flow = AppDelegate.shared?.auth?.accountFlow, flow.isAuthenticated, + let accountID = flow.currentIdentity?.id else { return nil } + let requestedTeamID = flow.confirmedTeamID + guard await flow.refreshBillingPlanAndReportSuccess() else { return nil } + // Only answer for the account that asked; a switch mid-check means + // this answer belongs to someone else. + guard !Task.isCancelled, + flow.currentIdentity?.id == accountID, + flow.confirmedTeamID == requestedTeamID else { return nil } // Same answer the Cloud tab uses, so both show Upgrade for Free plans. return flow.hasLoadedBillingPlan ? flow.isProActive : nil } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 508669a4f580..dad6ddd367b6 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -1716,6 +1716,7 @@ private let cmuxAuxiliaryWindowIdentifiers: Set = [ "cmux.newMachine", "cmux.settings", "cmux.about", + "cmux.cloud.welcome", "cmux.licenses", "cmux.browser-popup", "cmux.browserProfilePopoverDebug", diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 840306106272..f4d4fda1d7d1 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1078,6 +1078,10 @@ C8783B458C963A34D5A93FF3 /* CloudVPNSetupPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BE02DDB0D0E6C277EEF8E /* CloudVPNSetupPanelView.swift */; }; 5189C042A99401477E4DE49A /* CloudVPNSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3718948C68AF1E2610D73490 /* CloudVPNSetupTests.swift */; }; 59FECE3AAB392E8976A8D16A /* CloudVPNSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA45F7A29A3227884DC3929F /* CloudVPNSetupView.swift */; }; + 22D165EB062C603B0C3A0A82 /* CloudWelcomeHero.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA8C3407967FE40B15A41A3 /* CloudWelcomeHero.swift */; }; + 22EABB05BDD5DD6B52A4320C /* CloudWelcomeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6A733B8AA5EAFC5C30907F /* CloudWelcomeTests.swift */; }; + C0F314FC0BDC236EF220A9E3 /* CloudWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C632B1DEF42152DBA995A37 /* CloudWelcomeView.swift */; }; + D0158861231945076F7C049D /* CloudWelcomeWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09A9B78125D188EEE10DEC4 /* CloudWelcomeWindowController.swift */; }; 62AABF1720674956200EC388 /* CloudWireGuardHubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6544386D2BA373AF2133B58 /* CloudWireGuardHubTests.swift */; }; D12439060000000000000001 /* CloudWorkspaceBindingReconciliation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12439060000000000000002 /* CloudWorkspaceBindingReconciliation.swift */; }; D12439010000000000000001 /* CloudWorkspaceBindingRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12439010000000000000002 /* CloudWorkspaceBindingRegressionTests.swift */; }; @@ -5606,6 +5610,10 @@ 914BE02DDB0D0E6C277EEF8E /* CloudVPNSetupPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudVPNSetupPanelView.swift; sourceTree = ""; }; 3718948C68AF1E2610D73490 /* CloudVPNSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudVPNSetupTests.swift"; sourceTree = ""; }; EA45F7A29A3227884DC3929F /* CloudVPNSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudVPNSetupView.swift; sourceTree = ""; }; + 3CA8C3407967FE40B15A41A3 /* CloudWelcomeHero.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudWelcomeHero.swift; sourceTree = ""; }; + 6E6A733B8AA5EAFC5C30907F /* CloudWelcomeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudWelcomeTests.swift"; sourceTree = ""; }; + 6C632B1DEF42152DBA995A37 /* CloudWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudWelcomeView.swift; sourceTree = ""; }; + E09A9B78125D188EEE10DEC4 /* CloudWelcomeWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudWelcomeWindowController.swift; sourceTree = ""; }; C6544386D2BA373AF2133B58 /* CloudWireGuardHubTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudWireGuardHubTests.swift; sourceTree = ""; }; D12439060000000000000002 /* CloudWorkspaceBindingReconciliation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudWorkspaceBindingReconciliation.swift; sourceTree = ""; }; D12439010000000000000002 /* CloudWorkspaceBindingRegressionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudWorkspaceBindingRegressionTests.swift; sourceTree = ""; }; @@ -9654,6 +9662,9 @@ 5341596E18F2119066D6661C /* CloudRefreshMachinesButton.swift */, 5A6DBD58C3DCE74DB8832536 /* CloudActivationCoordinator.swift */, 5D03F7C677AFAB99364F7404 /* CloudMachinesEnablementView.swift */, + E09A9B78125D188EEE10DEC4 /* CloudWelcomeWindowController.swift */, + 6C632B1DEF42152DBA995A37 /* CloudWelcomeView.swift */, + 3CA8C3407967FE40B15A41A3 /* CloudWelcomeHero.swift */, 7AC64FE02675D0294E2CF115 /* MachinesPanelView+Activation.swift */, 2E642299D32B3539B81F7E90 /* MachinesPanelView+TeamScope.swift */, ); @@ -13379,6 +13390,7 @@ 272AE90B4C33027AB38F708E /* CloudActivationCoordinatorTests.swift */, F315CAB97E0954B5025E4A2D /* CloudCmdYActivationRoutingTests.swift */, 42E8F5614F1E09489C87CC50 /* CloudMachineLiftKeepOpenTests.swift */, + 6E6A733B8AA5EAFC5C30907F /* CloudWelcomeTests.swift */, FA81639A568A0FC187B60934 /* RestoredAgentNotificationPruneTests.swift */, 952544C6A0154DD480E89921 /* RemoteTmuxSocketErrorTextTests.swift */, ); @@ -14754,6 +14766,9 @@ 549C9947A81C58B3F6402500 /* CloudVPNSetupPanel.swift in Sources */, C8783B458C963A34D5A93FF3 /* CloudVPNSetupPanelView.swift in Sources */, 59FECE3AAB392E8976A8D16A /* CloudVPNSetupView.swift in Sources */, + 22D165EB062C603B0C3A0A82 /* CloudWelcomeHero.swift in Sources */, + C0F314FC0BDC236EF220A9E3 /* CloudWelcomeView.swift in Sources */, + D0158861231945076F7C049D /* CloudWelcomeWindowController.swift in Sources */, D12439060000000000000001 /* CloudWorkspaceBindingReconciliation.swift in Sources */, 0CF98EFC979F48F49B178E2E /* CloudWorkspaceCreationCoordinator.swift in Sources */, F3C8E0247F0746A1B76F4585 /* CloudWorkspaceCreationHost.swift in Sources */, @@ -17579,6 +17594,7 @@ 2D7F228001C24ABD865AFD66 /* CloudVMStateSnapshotComparisonTests.swift in Sources */, C12093000000000000000002 /* CloudVPNExplicitActivationTests.swift in Sources */, 5189C042A99401477E4DE49A /* CloudVPNSetupTests.swift in Sources */, + 22EABB05BDD5DD6B52A4320C /* CloudWelcomeTests.swift in Sources */, 62AABF1720674956200EC388 /* CloudWireGuardHubTests.swift in Sources */, D12439010000000000000001 /* CloudWorkspaceBindingRegressionTests.swift in Sources */, E7C1999FD0F7E0263AA9A53A /* CloudWorkspaceCreationRevealTests.swift in Sources */, diff --git a/cmuxTests/CloudWelcomeTests.swift b/cmuxTests/CloudWelcomeTests.swift new file mode 100644 index 000000000000..b2e4681f4b2c --- /dev/null +++ b/cmuxTests/CloudWelcomeTests.swift @@ -0,0 +1,43 @@ +import AppKit +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@Suite("Cloud welcome") +struct CloudWelcomeTests { + @Test("welcome owns close instead of the terminal behind it") + @MainActor + func welcomeOwnsCloseShortcut() { + let window = NSWindow( + contentRect: .zero, + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + window.isReleasedWhenClosed = false + defer { window.close() } + window.identifier = NSUserInterfaceItemIdentifier("cmux.cloud.welcome") + #expect(cmuxWindowShouldOwnCloseShortcut(window)) + } + + @Test("shows once, only while Cloud is offered and still off") + func presentsOnlyWhenUnseenAvailableAndOff() { + #expect(CloudWelcomeWindowController.shouldPresentAutomatically(seen: false, cloudAvailable: true, cloudEnabled: false)) + #expect(!CloudWelcomeWindowController.shouldPresentAutomatically(seen: true, cloudAvailable: true, cloudEnabled: false)) + #expect(!CloudWelcomeWindowController.shouldPresentAutomatically(seen: false, cloudAvailable: false, cloudEnabled: false)) + #expect(!CloudWelcomeWindowController.shouldPresentAutomatically(seen: false, cloudAvailable: true, cloudEnabled: true)) + } + + @Test("the one prominent button is the next step for this account") + func nextStepFollowsSignInAndPlan() { + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: false, isPlanKnown: false, isPro: false) == .signIn) + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: false, isPlanKnown: true, isPro: true) == .signIn) + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: true, isPlanKnown: false, isPro: false) == .openCloud) + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: true, isPlanKnown: true, isPro: false) == .upgrade) + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: true, isPlanKnown: true, isPro: true) == .enable) + } +} diff --git a/docs/posthog/billing-attribution.md b/docs/posthog/billing-attribution.md index 71eb2b7ea646..1a8fe863c064 100644 --- a/docs/posthog/billing-attribution.md +++ b/docs/posthog/billing-attribution.md @@ -32,6 +32,7 @@ Sources today: | `mac_settings_account_card` | Settings > Account card | | `mac_settings_cloud_machines` | Settings > Cloud machines billing | | `mac_machines_panel_requires_pro` | machines panel empty state | +| `mac_cloud_welcome` | "Introducing cmux Cloud" welcome window (Free plan) | | `mac_machines_panel_upgrade_nudge` | nudge under the create button | | `mac_machines_panel_trial_banner` | free-access countdown / expired banner | | `mac_machines_panel_machine_action` | row action that needs a paid plan |