From 759d8205f0c959f8b66869511da523e16289b078 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 31 Oct 2026 01:33:07 +0700 Subject: [PATCH 1/2] feat(support): ask for a license on the 30th day of use --- apple/TablePro/AppDelegate.swift | 1 + .../Services/Licensing/SupportLinks.swift | 3 + .../Support/SupportAskCoordinator.swift | 169 ++ .../Services/Support/SupportAskSchedule.swift | 45 + .../Services/Support/UsageDayCounter.swift | 46 + .../TablePro/Resources/Localizable.xcstrings | 2254 +++++++++-------- .../MainContentCoordinator+Registry.swift | 4 + .../Support/SupportAskSheetPresenter.swift | 84 + .../Views/Support/SupportAskView.swift | 64 + .../TablePro/Views/Support/SupportView.swift | 5 +- .../Licensing/SupportLinksTests.swift | 2 +- .../Support/SupportAskCoordinatorTests.swift | 224 ++ .../Support/SupportAskScheduleTests.swift | 77 + .../Support/UsageDayCounterTests.swift | 71 + 14 files changed, 1962 insertions(+), 1087 deletions(-) create mode 100644 apple/TablePro/Core/Services/Support/SupportAskCoordinator.swift create mode 100644 apple/TablePro/Core/Services/Support/SupportAskSchedule.swift create mode 100644 apple/TablePro/Core/Services/Support/UsageDayCounter.swift create mode 100644 apple/TablePro/Views/Support/SupportAskSheetPresenter.swift create mode 100644 apple/TablePro/Views/Support/SupportAskView.swift create mode 100644 apple/TableProTests/Core/Services/Support/SupportAskCoordinatorTests.swift create mode 100644 apple/TableProTests/Core/Services/Support/SupportAskScheduleTests.swift create mode 100644 apple/TableProTests/Core/Services/Support/UsageDayCounterTests.swift diff --git a/apple/TablePro/AppDelegate.swift b/apple/TablePro/AppDelegate.swift index 60145aca2b..b44751e201 100644 --- a/apple/TablePro/AppDelegate.swift +++ b/apple/TablePro/AppDelegate.swift @@ -145,6 +145,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { SyncCoordinator.shared.start() LinkedFolderWatcher.shared.start() TeamLibrarySyncCoordinator.shared.start() + SupportAskCoordinator.shared.start() Task { LicenseManager.shared.startPeriodicValidation() diff --git a/apple/TablePro/Core/Services/Licensing/SupportLinks.swift b/apple/TablePro/Core/Services/Licensing/SupportLinks.swift index 047650c1fb..d8729f13d0 100644 --- a/apple/TablePro/Core/Services/Licensing/SupportLinks.swift +++ b/apple/TablePro/Core/Services/Licensing/SupportLinks.swift @@ -16,6 +16,7 @@ internal enum SupportReferrer: Equatable { case aboutPanel case licenseSettings case activationSheet + case ask case featureGate(ProFeature) internal var parameterValue: String { @@ -28,6 +29,8 @@ internal enum SupportReferrer: Equatable { return "app-settings" case .activationSheet: return "app-activation" + case .ask: + return "app-ask" case .featureGate(let feature): return "app-gate-" + Self.slug(for: feature) } diff --git a/apple/TablePro/Core/Services/Support/SupportAskCoordinator.swift b/apple/TablePro/Core/Services/Support/SupportAskCoordinator.swift new file mode 100644 index 0000000000..e59b64135c --- /dev/null +++ b/apple/TablePro/Core/Services/Support/SupportAskCoordinator.swift @@ -0,0 +1,169 @@ +// +// SupportAskCoordinator.swift +// TablePro +// + +import AppKit +import Combine +import os + +/// Counts days of use and puts the license ask on the window in use when `SupportAskSchedule` +/// says it is due and the moment is quiet. +/// +/// It looks again whenever a window becomes key, the app becomes active, or the launch quiet +/// period ends, rather than polling: one of those happens soon after any moment the ask could use. +@MainActor +internal final class SupportAskCoordinator { + struct Environment { + var hasLicense: () -> Bool + var isQueryRunning: () -> Bool + var keyWindow: () -> NSWindow? + var isModalSessionRunning: () -> Bool + var openPricing: (URL) -> Void + var now: () -> Date + } + + enum Keys { + static let lastAskedAt = "com.TablePro.support.lastAskedAt" + static let lastAnswer = "com.TablePro.support.lastAnswer" + } + + private static let logger = Logger(subsystem: "com.TablePro", category: "SupportAsk") + + private let counter: UsageDayCounter + private let defaults: UserDefaults + private let presenter: any SupportAskPresenting + private let environment: Environment + private let calendar: Calendar + private let clock: any Clock + + private var launchedAt: Date? + private var subscriptions: Set = [] + private var quietPeriodTask: Task? + + init( + counter: UsageDayCounter, + defaults: UserDefaults, + presenter: any SupportAskPresenting, + environment: Environment, + calendar: Calendar = .autoupdatingCurrent, + clock: any Clock = ContinuousClock() + ) { + self.counter = counter + self.defaults = defaults + self.presenter = presenter + self.environment = environment + self.calendar = calendar + self.clock = clock + } + + func start() { + guard launchedAt == nil else { return } + launchedAt = environment.now() + counter.recordUse(at: environment.now()) + + let center = NotificationCenter.default + center.publisher(for: NSApplication.didBecomeActiveNotification) + .sink { [weak self] _ in self?.appBecameActive() } + .store(in: &subscriptions) + // Posted when the date rolls over while the app runs, from whichever thread noticed. + center.publisher(for: .NSCalendarDayChanged) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.dayChanged() } + .store(in: &subscriptions) + center.publisher(for: NSWindow.didBecomeKeyNotification) + .sink { [weak self] _ in self?.askIfDue() } + .store(in: &subscriptions) + + quietPeriodTask = Task { [weak self, clock] in + do { + try await clock.sleep(for: .seconds(SupportAskSchedule.launchQuietPeriod)) + } catch { + return + } + self?.askIfDue() + } + } + + var schedule: SupportAskSchedule { + SupportAskSchedule( + usageDays: counter.days, + hasLicense: environment.hasLicense(), + lastAskedAt: defaults.object(forKey: Keys.lastAskedAt) as? Date, + lastAnswer: defaults.string(forKey: Keys.lastAnswer).flatMap(SupportAskAnswer.init(rawValue:)) + ) + } + + /// Presents the ask when everything allows it. Each check is cheap, and the first that fails + /// ends the look. + func askIfDue() { + guard let launchedAt, !presenter.isPresenting else { return } + let now = environment.now() + guard schedule.isDue(at: now, calendar: calendar) else { return } + guard SupportAskSchedule.isQuietMoment( + launchedAt: launchedAt, + now: now, + isQueryRunning: environment.isQueryRunning() + ) else { return } + guard !environment.isModalSessionRunning(), + let window = environment.keyWindow(), + Self.canHostAsk(window) else { return } + + let presented = presenter.present(usageDays: counter.days, on: window) { [weak self] answer in + self?.record(answer) + } + guard presented else { return } + // Recorded on showing rather than on answering, so quitting with the sheet open still + // counts as an ask and the next launch does not repeat it. + defaults.set(now, forKey: Keys.lastAskedAt) + defaults.removeObject(forKey: Keys.lastAnswer) + Self.logger.info("Showed the license ask after \(self.counter.days, privacy: .public) days of use") + } + + /// Only a window someone works in carries the ask: a connection, a data file or the welcome + /// window. Settings, the Support window and other utility windows never do. + static func canHostAsk(_ window: NSWindow) -> Bool { + let identifier = window.identifier?.rawValue + let isWorkWindow = ConnectionWindowIdentity.isPrimaryWindow(identifier) + || ConnectionWindowIdentity.isDataFileWindow(identifier) + || ConnectionWindowIdentity.isWelcomeWindow(identifier) + return isWorkWindow + && window.isVisible + && !window.isMiniaturized + && window.attachedSheet == nil + && window.sheetParent == nil + } + + private func record(_ answer: SupportAskAnswer) { + defaults.set(answer.rawValue, forKey: Keys.lastAnswer) + if answer == .buy { + environment.openPricing(SupportLinks.pricing(.ask)) + } + } + + private func appBecameActive() { + counter.recordUse(at: environment.now()) + askIfDue() + } + + private func dayChanged() { + guard NSApp.isActive else { return } + counter.recordUse(at: environment.now()) + } +} + +extension SupportAskCoordinator { + static let shared = SupportAskCoordinator( + counter: .shared, + defaults: AppStorageEnvironment.shared.defaults, + presenter: SupportAskSheetPresenter(), + environment: Environment( + hasLicense: { LicenseManager.shared.supportAudience == .supporter }, + isQueryRunning: { MainContentCoordinator.isAnyQueryRunning }, + keyWindow: { NSApp.keyWindow }, + isModalSessionRunning: { NSApp.modalWindow != nil }, + openPricing: { NSWorkspace.shared.open($0) }, + now: { Date() } + ) + ) +} diff --git a/apple/TablePro/Core/Services/Support/SupportAskSchedule.swift b/apple/TablePro/Core/Services/Support/SupportAskSchedule.swift new file mode 100644 index 0000000000..77160abdd5 --- /dev/null +++ b/apple/TablePro/Core/Services/Support/SupportAskSchedule.swift @@ -0,0 +1,45 @@ +// +// SupportAskSchedule.swift +// TablePro +// + +import Foundation + +/// How someone answered the license ask. The raw values are stored, so they never change. +internal enum SupportAskAnswer: String, Equatable, Sendable { + case buy + case personal + case notNow +} + +/// When the app may ask someone without a license to buy one. +/// +/// The 30th day of use opens it. Each ask closes it again for 90 days, or for a year once the +/// person says they use TablePro personally, and a license closes it for good. Kept pure, so every +/// rule is tested without a window, a clock or a license. +internal struct SupportAskSchedule: Equatable, Sendable { + static let firstAskDay = 30 + static let daysBetweenAsks = 90 + static let daysAfterPersonalUse = 365 + /// Someone who just launched the app came to do something; the ask waits until they have. + static let launchQuietPeriod: TimeInterval = 5 * 60 + + var usageDays: Int + var hasLicense: Bool + var lastAskedAt: Date? + var lastAnswer: SupportAskAnswer? + + func isDue(at now: Date, calendar: Calendar) -> Bool { + guard !hasLicense, usageDays >= Self.firstAskDay else { return false } + guard let lastAskedAt else { return true } + let waitDays = lastAnswer == .personal ? Self.daysAfterPersonalUse : Self.daysBetweenAsks + guard let nextAsk = calendar.date(byAdding: .day, value: waitDays, to: lastAskedAt) else { return false } + return now >= nextAsk + } + + /// Whether this is a moment the ask may interrupt: past the launch quiet period, and with no + /// query running, so a sheet never lands on a result someone is waiting for. + static func isQuietMoment(launchedAt: Date, now: Date, isQueryRunning: Bool) -> Bool { + !isQueryRunning && now.timeIntervalSince(launchedAt) >= launchQuietPeriod + } +} diff --git a/apple/TablePro/Core/Services/Support/UsageDayCounter.swift b/apple/TablePro/Core/Services/Support/UsageDayCounter.swift new file mode 100644 index 0000000000..15a47a3289 --- /dev/null +++ b/apple/TablePro/Core/Services/Support/UsageDayCounter.swift @@ -0,0 +1,46 @@ +// +// UsageDayCounter.swift +// TablePro +// + +import Foundation + +/// Counts the distinct local days TablePro was in use, so the license ask can tell someone trying +/// the app from someone who has made it part of their week. +/// +/// The count stays in this Mac's defaults and is never sent anywhere. A day is stored as its +/// calendar date rather than a timestamp, so recording it twice is harmless however often the app +/// becomes active. +@MainActor +internal final class UsageDayCounter { + static let shared = UsageDayCounter(defaults: AppStorageEnvironment.shared.defaults) + + enum Keys { + static let days = "com.TablePro.support.usageDays" + static let lastDay = "com.TablePro.support.lastUsageDay" + } + + private let defaults: UserDefaults + private let calendar: Calendar + + init(defaults: UserDefaults, calendar: Calendar = .autoupdatingCurrent) { + self.defaults = defaults + self.calendar = calendar + } + + var days: Int { + defaults.integer(forKey: Keys.days) + } + + func recordUse(at date: Date) { + let day = dayStamp(of: date) + guard defaults.string(forKey: Keys.lastDay) != day else { return } + defaults.set(day, forKey: Keys.lastDay) + defaults.set(days + 1, forKey: Keys.days) + } + + private func dayStamp(of date: Date) -> String { + let parts = calendar.dateComponents([.year, .month, .day], from: date) + return String(format: "%04d-%02d-%02d", parts.year ?? 0, parts.month ?? 0, parts.day ?? 0) + } +} diff --git a/apple/TablePro/Resources/Localizable.xcstrings b/apple/TablePro/Resources/Localizable.xcstrings index f9c6cadf13..4a395d5fb4 100644 --- a/apple/TablePro/Resources/Localizable.xcstrings +++ b/apple/TablePro/Resources/Localizable.xcstrings @@ -74,792 +74,6 @@ } } } - }, - "\"%@\" is a name the database keeps for itself. Choose another name." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "\"%@\" is an attached database. Change its tables' structure from a connection that opens it." : { - - }, - "%1$@ column of %2$@" : { - - }, - "%1$d only left, %2$d only right, %3$d changed, %4$d same" : { - - }, - "%@" : { - - }, - "%@ connections do not support adding rows from Shortcuts." : { - - }, - "%@ has no column named %@." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ has no column named %2$@." - } - } - } - }, - "%@ holds values that cannot be compared, so the row cannot be found again." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "%@ is read-only, so rows cannot be added." : { - - }, - "%@, without checking stored values" : { - - }, - "A check needs an expression." : { - - }, - "A dictionary for each row, keyed by column" : { - - }, - "A field needs a type." : { - - }, - "A foreign key named “%@” already exists." : { - - }, - "A foreign key staged in this diagram uses “%1$@.%2$@”. Save or undo that key before renaming the column." : { - - }, - "A label cannot be empty." : { - - }, - "A label cannot be removed or moved once it exists." : { - - }, - "A larger binary value stays on the server until you open, copy or export it. Applies to tables with a primary key." : { - - }, - "A name cannot be empty." : { - - }, - "A row still holds the values the change was made on. Refresh the table before saving again." : { - - }, - "A transaction is open on this connection. Commit or roll it back in TablePro, then run the shortcut again." : { - - }, - "AI Policy controls in-app AI agents. External Clients controls Raycast, Cursor, Claude Desktop, other MCP clients, AppleScript, and Shortcuts. Effective scope is the minimum of the requesting token's scope and the External Clients level." : { - - }, - "Add Check" : { - - }, - "Add Field" : { - - }, - "Add Foreign Key…" : { - - }, - "Add Row to Table" : { - - }, - "Add Row to Table expects one row. Use Add Rows to Table for multiple rows." : { - - }, - "Add Rows to Table" : { - - }, - "Add a row to ${table}" : { - - }, - "Add check %@" : { - - }, - "Add check “%1$@”: %2$@" : { - - }, - "Add field “%1$@” %2$@" : { - - }, - "Add label “%1$@” after “%2$@”" : { - - }, - "Add label “%1$@” before “%2$@”" : { - - }, - "Add label “%@” at the end" : { - - }, - "Add multiple rows to a table on a saved connection. Provide the rows as a JSON array, CSV text, or a file." : { - - }, - "Add one row to a table on a saved connection. Provide the row as a JSON object or a CSV row." : { - - }, - "Add plugin_debugger to shared_preload_libraries and restart the server." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Add rows to ${table}" : { - - }, - "Added %lld rows to %@." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Added %1$lld rows to %2$@." - } - } - } - }, - "Added one row to %@." : { - - }, - "After" : { - - }, - "Allow NULL" : { - - }, - "Also drop the columns, functions and other objects that use this type" : { - - }, - "Ask the SSH server's administrator to turn on a method TablePro supports." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "At the End" : { - - }, - "Attach" : { - - }, - "Attach Database" : { - - }, - "Attach Database…" : { - - }, - "Attached Databases" : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Attached databases open only for a database file on this Mac, not for a database on a server." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Attached each time the connection opens. Reach a table as name.table, and browse each database in the sidebar." : { - - }, - "Base Type" : { - - }, - "Before" : { - - }, - "Binary values limit must be between 1 and 1,048,576 KB." : { - - }, - "Both results stopped at the row cap and are compared on the rows they hold. A row past the cap can show as only on one side." : { - - }, - "Build Join Query" : { - - }, - "Build Join Query…" : { - - }, - "Build a join query from the selected tables" : { - - }, - "Cannot Compare" : { - - }, - "Change field “%1$@” to %2$@" : { - - }, - "Check" : { - - }, - "Check the values already stored" : { - - }, - "Check the values already stored against the selected constraint" : { - - }, - "Checks" : { - - }, - "Choose a Result" : { - - }, - "Choose a Type" : { - - }, - "Choose the base type." : { - - }, - "Choose the database files to attach each time this connection opens." : { - - }, - "Compare Result With…" : { - - }, - "Compare Results" : { - - }, - "Compare With" : { - - }, - "Comparing" : { - - }, - "Comparing…" : { - - }, - "Constraint Name" : { - - }, - "Controls how external clients (Raycast, Cursor, Claude Desktop, AppleScript, Shortcuts) access this connection. Tokens cannot exceed this level even with full-access scope." : { - - }, - "Could Not Load Value" : { - - }, - "Could Not Refresh the Diagram" : { - - }, - "Could Not Stage the Change" : { - - }, - "Could not attach \"%@\": %@" : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Could not connect to the database: %@" : { - - }, - "Could not read the columns of %@." : { - - }, - "Couldn't Rename Type" : { - - }, - "Create Type" : { - - }, - "Database or Schema" : { - - }, - "Delete Foreign Key %@" : { - - }, - "Drop Constraint" : { - - }, - "Drop Field" : { - - }, - "Drop constraint “%@”" : { - - }, - "Drop field “%@”" : { - - }, - "Drop type “%@”?" : { - - }, - "Dropped" : { - - }, - "Edit Field" : { - - }, - "Every field needs a name." : { - - }, - "First Value" : { - - }, - "Inner Join" : { - - }, - "It supports these host key types: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Joins" : { - - }, - "Jump host %1$@ failed: %2$@" : { - - }, - "KB" : { - - }, - "Key %@ matches more than one row in the left result. Choose key columns that identify a single row, or match whole rows." : { - - }, - "Key %@ matches more than one row in the right result. Choose key columns that identify a single row, or match whole rows." : { - - }, - "Left" : { - - }, - "Left Join" : { - - }, - "Line %lld of the file is not valid %@ text." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Line %1$lld of the file is not valid %2$@ text." - } - } - } - }, - "Load binary values up to" : { - - }, - "Match Whole Rows" : { - - }, - "More than one attached database is named \"%@\"." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Name the type." : { - - }, - "Named by the server" : { - - }, - "New Name" : { - - }, - "New Type" : { - - }, - "No Attached Databases" : { - - }, - "No Columns in Common" : { - - }, - "No Result to Compare With" : { - - }, - "No data was provided to add." : { - - }, - "No foreign key links %@ to the tables before it, so it is cross joined." : { - - }, - "No Foreign Keys" : { - - }, - "No row holds the values it was loaded with. Another client may have changed or deleted it." : { - - }, - "No tables picked" : { - - }, - "None (Cross Join)" : { - - }, - "Not Null" : { - - }, - "Not loaded, %@" : { - - }, - "Not validated" : { - - }, - "Only Left" : { - - }, - "Only PL/pgSQL functions and procedures can be debugged." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "Only Right" : { - - }, - "Only in the left result, not compared: %@" : { - - }, - "Only in the right result, not compared: %@" : { - - }, - "Open in Query Tab" : { - - }, - "Opens a database connection in TablePro" : { - - }, - "Other rows hold exactly the same values, so the change cannot reach this row alone." : { - - }, - "Pick the result to compare this one with." : { - - }, - "Pick the tables to join. Their foreign keys become the join conditions." : { - - }, - "Position" : { - - }, - "Provide a JSON object or an array of objects." : { - - }, - "References" : { - - }, - "Refuse NULL" : { - - }, - "Remove Attached Database" : { - - }, - "Remove the comment" : { - - }, - "Remove the default" : { - - }, - "Remove this change from the queue" : { - - }, - "Remove “%@”" : { - - }, - "Rename Label" : { - - }, - "Rename Type" : { - - }, - "Rename field “%1$@” to “%2$@”" : { - - }, - "Rename label “%1$@” to “%2$@”" : { - - }, - "Rename the selected label" : { - - }, - "Rename to “%@”" : { - - }, - "Result %1$d: %2$@" : { - - }, - "Result Closed" : { - - }, - "Result Format" : { - - }, - "Right" : { - - }, - "Row (JSON or CSV)" : { - - }, - "Row Limit" : { - - }, - "Rows (JSON or CSV)" : { - - }, - "Rows with the same values in these columns are compared. With none chosen, whole rows are matched." : { - - }, - "Run ${query} on ${connection}" : { - - }, - "Run Query" : { - - }, - "Run a SQL statement on a saved connection and pass the result to the next action." : { - - }, - "Run another query in this tab, or pin a result in another tab." : { - - }, - "Save Type Changes" : { - - }, - "Second value" : { - - }, - "Section" : { - - }, - "Set the comment" : { - - }, - "Set the default to %@" : { - - }, - "Shortcuts" : { - - }, - "Shortcuts wants to add %1$lld rows to \"%2$@\"" : { - - }, - "Shortcuts wants to add a row to \"%@\"" : { - - }, - "Show Structure of %@" : { - - }, - "Some rows are no longer in the table, so their values could not be read." : { - - }, - "Statement" : { - - }, - "Subtype" : { - - }, - "Table Data" : { - - }, - "TablePro and the SSH server could not agree on how to encrypt the connection." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro and the SSH server have no cipher in common." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro and the SSH server have no key exchange or host key method in common." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro and the SSH server have no MAC algorithm in common." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro and the SSH server have no signature algorithm in common for this key." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro can't pass any value of type %2$@ to %1$@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro can't read %@. Check that this account can see its source." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro can't read this unit's source." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro can't turn text into a %2$@ value for %1$@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro signs RSA keys with rsa-sha2-256 or rsa-sha2-512, not with ssh-rsa, which uses SHA-1." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro supports these ciphers: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro supports these key exchange methods: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "TablePro supports these MAC algorithms: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "That connection no longer exists in TablePro." : { - - }, - "That result is no longer open. Choose another one." : { - - }, - "The CSV data has no header row." : { - - }, - "The JSON array must contain only objects." : { - - }, - "The attached database \"%@\" has no file." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The attached file \"%@\" has no name." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The changes were saved, but the type could not be read back. Reload before editing it again." : { - - }, - "The changes you have not saved will be lost." : { - - }, - "The client certificate and key could not be written to a temporary file." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The column “%1$@” is no longer in “%2$@”." : { - - }, - "The data could not be read: %@" : { - - }, - "The data has no values to insert into %@." : { - - }, - "The diagram is saving its changes. Make this change once the save has finished." : { - - }, - "The first column of the first row" : { - - }, - "The foreign key “%1$@” is no longer on “%2$@”." : { - - }, - "The left result stopped at the row cap and is compared on the %@ rows it holds. A row past the cap can show as only on the right." : { - - }, - "The name a statement uses for this database, as in name.table." : { - - }, - "The request timed out." : { - - }, - "The right result stopped at the row cap and is compared on the %@ rows it holds. A row past the cap can show as only on the left." : { - - }, - "The row could not be looked up: %@" : { - - }, - "The row has no loaded values to be found by." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The row's loaded values are missing, so it cannot be found again." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The saved list could not be read. Adding a database replaces it." : { - - }, - "The saved list of attached databases is not in a format TablePro can read." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The schema changes staged in this diagram will be lost." : { - - }, - "The server refused a change, so nothing was saved." : { - - }, - "The server refused a change. The changes before it were saved." : { - - }, - "The server offers only ciphers TablePro no longer supports: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The server offers only host key types TablePro no longer supports: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The server offers only key exchange methods TablePro no longer supports: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The server offers only MAC algorithms TablePro no longer supports: %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The server runs %@." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The SSH server closed the connection during the handshake." : { - - }, - "The SSH server ended the connection during the handshake: %@" : { - - }, - "The structure of a table in an attached database cannot be changed here." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "The text encoding is invalid." : { - - }, - "The two results share no column name, so there is nothing to compare." : { - - }, - "The type changed on the server since it was read. Reload it, then save again." : { - - }, - "This Tab" : { - - }, - "This cannot be undone. The drop fails while a column or other object still uses it." : { - - }, - "This cannot be undone. Without Cascade, the drop fails while a column, function or other type still uses it." : { - - }, - "This change does not apply to this kind of type." : { - - }, - "This _id reads as more than one value type in this collection, such as text and an ObjectId, so TablePro cannot tell which document the row names. Use a query." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, - "This connection's startup commands leave a transaction open, so rows added from Shortcuts could not be committed." : { - - }, - "This database cannot make the change “%@”." : { - - }, - "This database reports no tables to join." : { - - }, - "This server cannot rename a label." : { - - }, - "Too many rows. Add up to %lld rows at a time." : { - - }, - "Turn off to add the check now and validate the stored values later." : { - - }, - "Validate" : { - - }, - "Validate constraint “%@”" : { - - }, - "Validated on save" : { - - }, - "Was %@" : { - - }, - "Was “%@”" : { - - }, - "Whole row" : { - - }, - "^[%lld change](inflect: true)" : { - }, "__name__ is the document's ID in this collection, such as alice." : { "extractionState" : "manual", @@ -1016,45 +230,6 @@ } } } - }, - "connection" : { - - }, - "database" : { - - }, - "import" : { - - }, - "insert" : { - - }, - "left value, differs" : { - - }, - "only in the left result" : { - - }, - "only in the right result" : { - - }, - "open" : { - - }, - "query" : { - - }, - "right value, differs" : { - - }, - "row" : { - - }, - "rows" : { - - }, - "select" : { - }, "—" : { "extractionState" : "stale", @@ -1226,33 +401,6 @@ }, ":%@" : { "shouldTranslate" : false - }, - "“%@” already has a column with that name." : { - - }, - "“%@” is already a constraint." : { - - }, - "“%@” is already a field." : { - - }, - "“%@” is already a label." : { - - }, - "“%@” is already validated." : { - - }, - "“%@” is listed twice." : { - - }, - "“%@” is not a constraint any more." : { - - }, - "“%@” is not a field any more." : { - - }, - "“%@” is not a label any more." : { - }, "…and %d more lines" : { "localizations" : { @@ -1581,6 +729,9 @@ } } } + }, + "“%@” already has a column with that name." : { + }, "“%@” and its token are removed from this Mac. Sessions lose its tools." : { "localizations" : { @@ -1597,10 +748,29 @@ }, "“%@” holds no file to import." : { + }, + "\"%@\" is a name the database keeps for itself. Choose another name." : { + "extractionState" : "manual", + "generatesSymbol" : false }, "\"%@\" is a reserved theme identifier." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "“%@” is already a constraint." : { + + }, + "“%@” is already a field." : { + + }, + "“%@” is already a label." : { + + }, + "“%@” is already validated." : { + + }, + "\"%@\" is an attached database. Change its tables' structure from a connection that opens it." : { + }, "“%@” is compressed." : { "localizations" : { @@ -1635,6 +805,9 @@ } } } + }, + "“%@” is listed twice." : { + }, "“%@” is no longer at that location." : { "localizations" : { @@ -1661,6 +834,9 @@ "\"%@\" is not a column. Name it as family:qualifier." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "“%@” is not a constraint any more." : { + }, "\"%@\" is not a DynamoDB action this editor runs" : { "extractionState" : "manual", @@ -1685,6 +861,9 @@ } } } + }, + "“%@” is not a field any more." : { + }, "\"%@\" is not a file." : { "extractionState" : "manual", @@ -1716,6 +895,9 @@ }, "“%@” is not a host or host:port" : { + }, + "“%@” is not a label any more." : { + }, "\"%@\" is not a library." : { "localizations" : { @@ -2511,12 +1693,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "(%lld instructions)" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -3037,6 +2213,9 @@ } } } + }, + "%@" : { + }, "%@\nPreview tab. Double-click to keep it open." : { "localizations" : { @@ -3941,6 +3120,9 @@ } } } + }, + "%@ connections do not support adding rows from Shortcuts." : { + }, "%@ connections open a server, not a database file, so there is nothing to fetch over SSH." : { "localizations" : { @@ -4322,6 +3504,16 @@ } } }, + "%@ has no column named %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ has no column named %2$@." + } + } + } + }, "%@ has no column order of its own to change." : { "localizations" : { "fr" : { @@ -4391,6 +3583,10 @@ } } }, + "%@ holds values that cannot be compared, so the row cannot be found again." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "%@ in the workbook expands to far more data than it holds, so it was not read." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -4828,6 +4024,9 @@ } } } + }, + "%@ is read-only, so rows cannot be added." : { + }, "%@ is required" : { "localizations" : { @@ -6619,6 +5818,9 @@ } } } + }, + "%@, without checking stored values" : { + }, "%@: %@" : { "extractionState" : "manual", @@ -7555,12 +6757,6 @@ "formatSpecifier" : "d", "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%arg points" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -7625,6 +6821,9 @@ } } } + }, + "%1$@ column of %2$@" : { + }, "%1$@ could not be read again before generating the script. %2$@" : { "localizations" : { @@ -8606,6 +7805,9 @@ } } } + }, + "%1$d only left, %2$d only right, %3$d changed, %4$d same" : { + }, "%1$d rows match. The first %2$d are listed." : { "localizations" : { @@ -9225,12 +8427,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d octets" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -9347,12 +8543,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d modifications" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -10628,12 +9818,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d lignes contiennent NULL dans une colonne de clé et ont été exclues. Choisissez une clé sans NULL pour les comparer." - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -10715,12 +9899,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%d lignes correspondent. Les lignes identiques sont comptées sans être listées, pour qu’aucune différence ne soit noyée dans cette liste." - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -11758,12 +10936,6 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%arg lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -11826,12 +10998,6 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%arg lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -12726,12 +11892,6 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%arg lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -12794,12 +11954,6 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%arg lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -13010,18 +12164,15 @@ } } } + }, + "^[%lld change](inflect: true)" : { + }, "^[%lld column](inflect: true)" : { "localizations" : { "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld colonnes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -13074,12 +12225,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld connexions utilisent ce profil. Elles en conservent les identifiants." - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -13115,12 +12260,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -13143,12 +12282,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -13201,12 +12334,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld lignes à exporter" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -13229,12 +12356,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld tables à exporter" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -15778,6 +14899,9 @@ } } } + }, + "A check needs an expression." : { + }, "A client certificate needs its client key. On the connection's Network tab, choose the client key, or clear the client certificate." : { "extractionState" : "manual", @@ -16251,6 +15375,9 @@ } } } + }, + "A dictionary for each row, keyed by column" : { + }, "A different file already exists at %@." : { "extractionState" : "stale", @@ -16384,6 +15511,9 @@ } } } + }, + "A field needs a type." : { + }, "A file" : { "localizations" : { @@ -16451,6 +15581,9 @@ } } } + }, + "A foreign key named “%@” already exists." : { + }, "A foreign key needs as many referenced columns as it has columns." : { "localizations" : { @@ -16461,6 +15594,9 @@ } } } + }, + "A foreign key staged in this diagram uses “%1$@.%2$@”. Save or undo that key before renaming the column." : { + }, "A foreign table" : { "localizations" : { @@ -16648,6 +15784,15 @@ } } } + }, + "A label cannot be empty." : { + + }, + "A label cannot be removed or moved once it exists." : { + + }, + "A larger binary value stays on the server until you open, copy or export it. Applies to tables with a primary key." : { + }, "A local secondary index is part of its table and is removed only with the table." : { "extractionState" : "manual", @@ -16740,6 +15885,9 @@ } } } + }, + "A name cannot be empty." : { + }, "A new child needs at least one value. The Realtime Database does not store empty nodes." : { "extractionState" : "manual", @@ -17170,6 +16318,9 @@ "A row needs a key. Bigtable has no empty row key." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "A row still holds the values the change was made on. Refresh the table before saving again." : { + }, "A row the filter hides holds %@ in column “%@”, which %@ cannot store. Clear the filter to find it, or choose Save As and pick UTF-8." : { "localizations" : { @@ -17942,6 +17093,9 @@ }, "A transaction is open on this connection, and its uncommitted rows aren't in these counts." : { + }, + "A transaction is open on this connection. Commit or roll it back in TablePro, then run the shortcut again." : { + }, "A tunnel forwards one address, and the node Sentinel names is on the server's own network, so Sentinel mode cannot run through one. Connect to a data node directly, or turn the tunnel off." : { "extractionState" : "manual", @@ -19865,6 +19019,9 @@ } } } + }, + "Add a row to ${table}" : { + }, "Add a saved query to watch its result here." : { @@ -19960,6 +19117,15 @@ } } } + }, + "Add Check" : { + + }, + "Add check “%1$@”: %2$@" : { + + }, + "Add check %@" : { + }, "Add Check Constraint" : { "localizations" : { @@ -20329,6 +19495,12 @@ } } } + }, + "Add Field" : { + + }, + "Add field “%1$@” %2$@" : { + }, "Add filter" : { "localizations" : { @@ -20532,6 +19704,9 @@ }, "Add foreign key “%@”" : { + }, + "Add Foreign Key…" : { + }, "Add Global Index" : { "extractionState" : "manual", @@ -20711,7 +19886,6 @@ } }, "Add Label" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -20720,6 +19894,15 @@ } } } + }, + "Add label “%@” at the end" : { + + }, + "Add label “%1$@” after “%2$@”" : { + + }, + "Add label “%1$@” before “%2$@”" : { + }, "Add Label After…" : { "extractionState" : "stale", @@ -20797,6 +19980,16 @@ } } }, + "Add multiple rows to a table on a saved connection. Provide the rows as a JSON array, CSV text, or a file." : { + + }, + "Add one row to a table on a saved connection. Provide the row as a JSON object or a CSV row." : { + + }, + "Add plugin_debugger to shared_preload_libraries and restart the server." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "Add Provider" : { "extractionState" : "stale", "localizations" : { @@ -20927,6 +20120,15 @@ } } } + }, + "Add Row to Table" : { + + }, + "Add Row to Table expects one row. Use Add Rows to Table for multiple rows." : { + + }, + "Add rows to ${table}" : { + }, "Add rows to it" : { "localizations" : { @@ -20937,6 +20139,9 @@ } } } + }, + "Add Rows to Table" : { + }, "Add Rule" : { "localizations" : { @@ -21214,6 +20419,19 @@ } } } + }, + "Added %lld rows to %@." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Added %1$lld rows to %2$@." + } + } + } + }, + "Added one row to %@." : { + }, "Adding check constraint %@ scans the table and fails if any existing row violates it." : { "localizations" : { @@ -21752,9 +20970,15 @@ } } } + }, + "After" : { + }, "After keyword expansion, Tab moves through fields such as %1$@, and %2$@ marks where the cursor ends up." : { + }, + "After the values already in %@" : { + }, "Agent" : { "localizations" : { @@ -22300,6 +21524,7 @@ } }, "AI Policy controls in-app AI agents. External Clients controls Raycast, Cursor, Claude Desktop, other MCP clients, and AppleScript. Effective scope is the minimum of the requesting token's scope and the External Clients level." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -22338,6 +21563,9 @@ } } } + }, + "AI Policy controls in-app AI agents. External Clients controls Raycast, Cursor, Claude Desktop, other MCP clients, AppleScript, and Shortcuts. Effective scope is the minimum of the requesting token's scope and the External Clients level." : { + }, "AI Provider" : { "extractionState" : "stale", @@ -22539,9 +21767,6 @@ } } } - }, - "After the values already in %@" : { - }, "Airplane" : { "extractionState" : "manual", @@ -22849,12 +22074,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toutes les %lld lignes sélectionnées" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -23861,6 +23080,9 @@ } } } + }, + "Allow NULL" : { + }, "Allowed Connections" : { "localizations" : { @@ -24075,6 +23297,9 @@ }, "already saved" : { + }, + "Also drop the columns, functions and other objects that use this type" : { + }, "Also handles" : { "localizations" : { @@ -28613,6 +27838,10 @@ } } }, + "Ask the SSH server's administrator to turn on a method TablePro supports." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "Ask your administrator to register postgres/host.domain.com in the realm, and connect by hostname rather than IP address." : { "extractionState" : "manual", "generatesSymbol" : false @@ -28820,6 +28049,9 @@ } } } + }, + "At the End" : { + }, "At the end of the line" : { @@ -28860,6 +28092,9 @@ "Atom" : { "extractionState" : "manual", "generatesSymbol" : false + }, + "Attach" : { + }, "Attach a connection to this conversation first." : { "localizations" : { @@ -28910,6 +28145,12 @@ } } } + }, + "Attach Database" : { + + }, + "Attach Database…" : { + }, "Attached %1$d of %2$d files. %3$@" : { "localizations" : { @@ -28920,6 +28161,17 @@ } } } + }, + "Attached Databases" : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "Attached databases open only for a database file on this Mac, not for a database on a server." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "Attached each time the connection opens. Reach a table as name.table, and browse each database in the sidebar." : { + }, "Attachments" : { "extractionState" : "stale", @@ -31477,6 +30729,9 @@ "Bar Chart" : { "extractionState" : "manual", "generatesSymbol" : false + }, + "Base Type" : { + }, "Base URL" : { "localizations" : { @@ -31684,6 +30939,9 @@ } } } + }, + "Before" : { + }, "BEFORE, AFTER, or INSTEAD OF" : { "localizations" : { @@ -31988,6 +31246,9 @@ }, "Binary File" : { + }, + "Binary values limit must be between 1 and 1,048,576 KB." : { + }, "Block" : { "extractionState" : "stale", @@ -32372,6 +31633,9 @@ } } } + }, + "Both results stopped at the row cap and are compared on the rows they hold. A row past the cap can show as only on one side." : { + }, "Both sides are one connection, so the server copies the rows itself. There is no row-by-row progress, and Stop cannot interrupt it." : { "localizations" : { @@ -32768,6 +32032,15 @@ "Bug" : { "extractionState" : "manual", "generatesSymbol" : false + }, + "Build a join query from the selected tables" : { + + }, + "Build Join Query" : { + + }, + "Build Join Query…" : { + }, "Build the SQL that brings the target in line" : { "localizations" : { @@ -34387,6 +33660,9 @@ }, "Cannot be NULL." : { + }, + "Cannot Compare" : { + }, "Cannot connect to Ollama at %@. Is Ollama running?" : { "localizations" : { @@ -35817,6 +35093,9 @@ } } } + }, + "Change field “%1$@” to %2$@" : { + }, "Change File" : { "extractionState" : "stale", @@ -36108,6 +35387,7 @@ } }, "Change Primary Key" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -36824,6 +36104,7 @@ } }, "Charset:" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -37184,6 +36465,9 @@ } } } + }, + "Check" : { + }, "Check a query for correctness, cost, and risk against the live schema before running it" : { "localizations" : { @@ -37591,6 +36875,12 @@ } } } + }, + "Check the values already stored" : { + + }, + "Check the values already stored against the selected constraint" : { + }, "Checkered Flag" : { "extractionState" : "manual", @@ -37741,6 +37031,9 @@ "Checkmark" : { "extractionState" : "manual", "generatesSymbol" : false + }, + "Checks" : { + }, "Chinook (Sample)" : { "localizations" : { @@ -38144,6 +37437,7 @@ }, "Choose a location to save the diagram as PNG." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -38292,6 +37586,9 @@ } } } + }, + "Choose a Result" : { + }, "Choose a SAP HANA schema first." : { "extractionState" : "manual", @@ -38631,6 +37928,9 @@ } } } + }, + "Choose a Type" : { + }, "Choose AI provider and model" : { "localizations" : { @@ -39047,6 +38347,9 @@ } } } + }, + "Choose the base type." : { + }, "Choose the client certificate and key on the connection's Network tab." : { "extractionState" : "manual", @@ -39099,6 +38402,9 @@ } } } + }, + "Choose the database files to attach each time this connection opens." : { + }, "Choose the extension's file again in Edit Connection." : { "extractionState" : "manual", @@ -39135,6 +38441,7 @@ } }, "Choose Type" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -39175,6 +38482,7 @@ } }, "Choose Value" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -43480,6 +42788,7 @@ } }, "Collation:" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -45346,9 +44655,6 @@ } } } - }, - "Comment:" : { - }, "Comments" : { "localizations" : { @@ -45827,6 +45133,12 @@ } } } + }, + "Compare Result With…" : { + + }, + "Compare Results" : { + }, "Compare structure or data" : { "localizations" : { @@ -45997,6 +45309,9 @@ } } } + }, + "Compare With" : { + }, "Compared %@ keys in key order. Rows past the limit were not read on either side, and rows with NULL in a key column are not read under a limit." : { "localizations" : { @@ -46169,6 +45484,9 @@ } } } + }, + "Comparing" : { + }, "Comparing only. Changes applied earlier stay in the target." : { "localizations" : { @@ -46219,6 +45537,9 @@ } } } + }, + "Comparing…" : { + }, "Comparison cancelled." : { "localizations" : { @@ -46705,6 +46026,7 @@ }, "Configure an active provider to enable inline suggestions." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -47159,12 +46481,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecter %lld connexions" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -48062,6 +47378,9 @@ } } } + }, + "connection" : { + }, "Connection" : { "localizations" : { @@ -49764,6 +49083,9 @@ } } } + }, + "Constraint Name" : { + }, "Constraints" : { "localizations" : { @@ -50110,8 +49432,12 @@ } } } + }, + "Controls how external clients (Raycast, Cursor, Claude Desktop, AppleScript, Shortcuts) access this connection. Tokens cannot exceed this level even with full-access scope." : { + }, "Controls how external clients (Raycast, Cursor, Claude Desktop, AppleScript) access this connection. Tokens cannot exceed this level even with full-access scope." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -50944,9 +50270,6 @@ } } } - }, - "Copy CREATE INDEX" : { - }, "Copy Cell Value" : { @@ -51113,6 +50436,9 @@ }, "Copy Count" : { + }, + "Copy CREATE INDEX" : { + }, "Copy DDL" : { "localizations" : { @@ -52603,6 +51929,10 @@ } } }, + "Could not attach \"%@\": %@" : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "Could not attach %1$d files. %2$@" : { "localizations" : { "fr" : { @@ -52744,6 +52074,7 @@ }, "Could not build the column reorder for this table. If this engine's driver was installed before column reorder shipped, update it in Settings > Plugins." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -52800,6 +52131,7 @@ }, "Could not build the table rebuild for this change. If this engine's driver was installed before foreign key editing shipped, update it in Settings > Plugins." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -53016,6 +52348,9 @@ } } } + }, + "Could not connect to the database: %@" : { + }, "Could not create %@." : { "localizations" : { @@ -53666,6 +53001,9 @@ } } } + }, + "Could Not Load Value" : { + }, "Could not make another value for (%@) that does not repeat. Widen its range or generate fewer rows." : { @@ -54176,6 +53514,9 @@ } } } + }, + "Could not read the columns of %@." : { + }, "Could not read the file. It may have been deleted or moved." : { "localizations" : { @@ -54338,6 +53679,9 @@ }, "Could not read which rows are already in the table" : { + }, + "Could Not Refresh the Diagram" : { + }, "Could not release the file" : { "localizations" : { @@ -54541,6 +53885,9 @@ } } } + }, + "Could Not Stage the Change" : { + }, "Could not start a new TablePro instance. Quit and reopen manually." : { "localizations" : { @@ -55059,6 +54406,9 @@ }, "Couldn't Rename the Theme" : { + }, + "Couldn't Rename Type" : { + }, "Couldn't Restore Version" : { "localizations" : { @@ -56256,6 +55606,9 @@ } } } + }, + "Create Type" : { + }, "Create your own slash commands. Use {{query}}, {{schema}}, {{database}}, or {{body}} in the template to insert chat context at runtime." : { "localizations" : { @@ -56430,6 +55783,7 @@ } }, "Creating…" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -58494,6 +57848,9 @@ } } } + }, + "database" : { + }, "Database" : { "extractionState" : "manual", @@ -59494,6 +58851,9 @@ } } } + }, + "Database or Schema" : { + }, "Database Schema" : { "extractionState" : "manual", @@ -62131,12 +61491,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Supprimer %lld connexions" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -62595,6 +61949,9 @@ }, "Delete foreign key “%@”" : { + }, + "Delete Foreign Key %@" : { + }, "Delete Group" : { "localizations" : { @@ -63038,6 +62395,7 @@ } }, "Delete Selected" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -67611,6 +66969,12 @@ } } } + }, + "Drop Constraint" : { + + }, + "Drop constraint “%@”" : { + }, "Drop Database" : { "extractionState" : "stale", @@ -67772,6 +67136,12 @@ } } } + }, + "Drop Field" : { + + }, + "Drop field “%@”" : { + }, "Drop Foreign Table" : { "localizations" : { @@ -68291,6 +67661,9 @@ }, "Drop Type" : { + }, + "Drop type “%@”?" : { + }, "Drop Type…" : { @@ -68374,6 +67747,9 @@ } } } + }, + "Dropped" : { + }, "dropping %@ '%@'" : { "localizations" : { @@ -69934,6 +69310,9 @@ } } } + }, + "Edit Field" : { + }, "Edit Foreign Key" : { "localizations" : { @@ -70192,16 +69571,16 @@ "Edit Search" : { }, - "Edit Settings" : { + "Edit Sequence" : { }, - "Edit Table Options" : { + "Edit Sequence…" : { }, - "Edit Sequence" : { + "Edit Settings" : { }, - "Edit Sequence…" : { + "Edit Table Options" : { }, "Edit the connection and choose its database type." : { @@ -70918,6 +70297,7 @@ } }, "Embedded analytical SQL" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -72130,6 +71510,7 @@ } }, "Engine:" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -72530,6 +71911,7 @@ } }, "Enter table name" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -73817,6 +73199,13 @@ } } }, + "Estimated impact %.1f%%" : { + + }, + "Estimated Plan" : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "Estimated row count" : { "localizations" : { "fr" : { @@ -74057,13 +73446,6 @@ } } }, - "Estimated impact %.1f%%" : { - - }, - "Estimated Plan" : { - "extractionState" : "manual", - "generatesSymbol" : false - }, "etcd cannot filter with %@." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -74418,6 +73800,9 @@ } } } + }, + "Every field needs a name." : { + }, "Every file in this folder matches the last commit." : { @@ -75891,12 +75276,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld instructions exécutées" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -77257,6 +76636,7 @@ } }, "Export as PNG" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -80499,6 +79879,7 @@ } }, "Failed to render the diagram image." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -82965,12 +82346,6 @@ "formatSpecifier" : "lld", "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%arg lignes" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -83253,6 +82628,7 @@ } }, "Find in file" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -83263,6 +82639,7 @@ } }, "Find in loaded rows" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -83853,6 +83230,9 @@ } } } + }, + "First Value" : { + }, "Fish" : { "extractionState" : "manual", @@ -85241,6 +84621,7 @@ }, "Formatter error: %@" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -85412,10 +84793,10 @@ } } }, - "From" : { + "Freeze Up to This Column" : { }, - "Freeze Up to This Column" : { + "From" : { }, "From %@…" : { @@ -90462,6 +89843,46 @@ } } }, + "I Use It Personally" : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je l’utilise à titre personnel" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "개인적으로 사용합니다" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişisel Olarak Kullanıyorum" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tôi dùng cá nhân" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "我个人使用" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "我個人使用" + } + } + } + }, "iCloud asked TablePro to wait. It tries again on its own shortly." : { }, @@ -90508,9 +89929,6 @@ }, "iCloud could not complete the sync. TablePro tries again on the next sync." : { - }, - "iCloud Full" : { - }, "iCloud Is Not Ready" : { @@ -90565,6 +89983,7 @@ } }, "iCloud Sync is active" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -90878,6 +90297,46 @@ } } }, + "If TablePro is part of your work now, please buy a license. Licenses are what pay for the next release." : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si TablePro fait désormais partie de votre travail, merci d’acheter une licence. Ce sont les licences qui financent la prochaine version." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro가 이제 업무의 일부라면 라이선스를 구매해 주세요. 다음 릴리스는 라이선스로 만들어집니다." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro artık işinizin bir parçasıysa lütfen bir lisans satın alın. Bir sonraki sürümü lisanslar finanse ediyor." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nếu TablePro đã là một phần công việc của bạn, hãy mua một giấy phép. Giấy phép là thứ chi trả cho bản phát hành tiếp theo." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如果 TablePro 已经是你工作的一部分,请购买许可证。许可证支付了下一个版本的开发。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "如果 TablePro 已經是你工作的一部分,請購買授權。授權支付了下一個版本的開發。" + } + } + } + }, "If the connection is already there" : { "localizations" : { "fr" : { @@ -91207,6 +90666,9 @@ } } } + }, + "import" : { + }, "Import" : { "extractionState" : "manual", @@ -94430,6 +93892,12 @@ }, "Inline suggestions and Review, Explain, Optimize and Fix run on this Mac and send nothing over the network." : { + }, + "Inner Join" : { + + }, + "insert" : { + }, "Insert" : { "extractionState" : "manual", @@ -97327,11 +96795,15 @@ "It stays on this Mac and never syncs, because it can name a command to run." : { }, - "It stopped after adding ^[%lld row](inflect: true)." : { + "It stays on this Mac and never syncs, because it names where your Vault token is sent." : { }, - "It stays on this Mac and never syncs, because it names where your Vault token is sent." : { + "It stopped after adding ^[%lld row](inflect: true)." : { + }, + "It supports these host key types: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false }, "It was changed outside TablePro and no longer loads. Settings > Appearance lists it under Not Loaded with the reason." : { @@ -97521,6 +96993,9 @@ }, "Join condition:" : { + }, + "Joins" : { + }, "Joins this column with the one to its right." : { "localizations" : { @@ -97797,6 +97272,9 @@ } } } + }, + "Jump host %1$@ failed: %2$@" : { + }, "Jump host configuration is invalid" : { "localizations" : { @@ -98051,6 +97529,9 @@ } } } + }, + "KB" : { + }, "Keep a Table Open" : { "localizations" : { @@ -98673,6 +98154,12 @@ } } } + }, + "Key %@ matches more than one row in the left result. Choose key columns that identify a single row, or match whole rows." : { + + }, + "Key %@ matches more than one row in the right result. Choose key columns that identify a single row, or match whole rows." : { + }, "Key %@ was not found in the secret JSON." : { "localizations" : { @@ -99404,13 +98891,6 @@ } } }, - "This sequence's settings could not be read, so they cannot be changed here." : { - - }, - "This server can't show the default value of %@, so enter a value for it." : { - "extractionState" : "manual", - "generatesSymbol" : false - }, "keyword: %@" : { "localizations" : { "fr" : { @@ -99726,7 +99206,6 @@ } }, "Labels" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -100441,6 +99920,7 @@ } }, "Last synced %@" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -100954,13 +100434,7 @@ "extractionState" : "manual", "generatesSymbol" : false }, - "Set Next Value" : { - - }, - "Start" : { - - }, - "The increment cannot be zero." : { + "Left" : { }, "left curly bracket" : { @@ -101003,6 +100477,9 @@ } } } + }, + "Left Join" : { + }, "Left out" : { "localizations" : { @@ -101101,6 +100578,9 @@ } } } + }, + "left value, differs" : { + }, "Legacy UUID Encoding" : { "extractionState" : "manual", @@ -101441,6 +100921,7 @@ } }, "License expired, sync paused" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -101728,6 +101209,7 @@ } }, "License not verified in 30 days, sync paused. Click to check again." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -102491,6 +101973,16 @@ }, "Line %d" : { + }, + "Line %lld of the file is not valid %@ text." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Line %1$lld of the file is not valid %2$@ text." + } + } + } }, "Line break" : { "extractionState" : "manual", @@ -104669,6 +104161,9 @@ } } } + }, + "Load binary values up to" : { + }, "Load Extensions" : { "localizations" : { @@ -107304,6 +106799,9 @@ } } } + }, + "Match Whole Rows" : { + }, "Matched name" : { "localizations" : { @@ -110017,12 +109515,6 @@ } } } - }, - "Missing Index" : { - - }, - "Missing Indexes" : { - }, "Missing argument: %@" : { "extractionState" : "manual", @@ -110065,6 +109557,12 @@ } } } + }, + "Missing Index" : { + + }, + "Missing Indexes" : { + }, "Missing Profile" : { "localizations" : { @@ -110792,6 +110290,10 @@ } } }, + "More than one attached database is named \"%@\"." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "More than one check constraint is named %@. Change or delete all of them in the same save." : { "localizations" : { "fr" : { @@ -112741,6 +112243,9 @@ "Name the table before the key, as in GET events/user#42." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "Name the type." : { + }, "Name this selection" : { "localizations" : { @@ -112791,6 +112296,9 @@ } } } + }, + "Named by the server" : { + }, "Namespace" : { "extractionState" : "manual", @@ -113318,7 +112826,6 @@ } }, "New" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -114001,9 +113508,6 @@ } } } - }, - "New Sequence…" : { - }, "New image" : { "extractionState" : "manual", @@ -114059,6 +113563,9 @@ } } } + }, + "New Name" : { + }, "New password:" : { "localizations" : { @@ -114192,6 +113699,9 @@ }, "New Seed" : { + }, + "New Sequence…" : { + }, "New Session" : { "localizations" : { @@ -114405,7 +113915,6 @@ } }, "New Table" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -114610,9 +114119,11 @@ } } } + }, + "New Type" : { + }, "New Type…" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -115297,6 +114808,9 @@ } } } + }, + "No Attached Databases" : { + }, "No available local port for SSH tunnel" : { "localizations" : { @@ -115676,6 +115190,9 @@ } } } + }, + "No Columns in Common" : { + }, "No columns in common." : { "localizations" : { @@ -116259,6 +115776,9 @@ } } } + }, + "No data was provided to add." : { + }, "No database connection" : { "localizations" : { @@ -116581,6 +116101,7 @@ } }, "No DDL available" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -117039,6 +116560,12 @@ } } } + }, + "No foreign key links %@ to the tables before it, so it is cross joined." : { + + }, + "No Foreign Keys" : { + }, "No Foreign Keys Yet" : { "localizations" : { @@ -117201,6 +116728,7 @@ } }, "No iCloud" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -119233,6 +118761,9 @@ } } } + }, + "No Result to Compare With" : { + }, "No Results" : { "localizations" : { @@ -119343,6 +118874,9 @@ } } } + }, + "No row holds the values it was loaded with. Another client may have changed or deleted it." : { + }, "No Row Selected" : { "localizations" : { @@ -119935,6 +119469,9 @@ } } } + }, + "No sequences" : { + }, "No servers yet." : { "localizations" : { @@ -120257,7 +119794,6 @@ } }, "No Tables" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -120346,6 +119882,9 @@ } } } + }, + "No tables picked" : { + }, "No tables selected for export" : { "localizations" : { @@ -120728,9 +120267,6 @@ } } } - }, - "No sequences" : { - }, "No valid rows found in clipboard data." : { "localizations" : { @@ -121190,6 +120726,9 @@ } } } + }, + "None (Cross Join)" : { + }, "None (Top Level)" : { "localizations" : { @@ -122390,6 +121929,9 @@ } } } + }, + "Not loaded, %@" : { + }, "Not Now" : { "localizations" : { @@ -122430,6 +121972,9 @@ } } } + }, + "Not Null" : { + }, "NOT NULL" : { "extractionState" : "stale", @@ -122992,6 +122537,9 @@ } } } + }, + "Not validated" : { + }, "Not visible" : { "localizations" : { @@ -126120,6 +125668,18 @@ } } } + }, + "only in the left result" : { + + }, + "Only in the left result, not compared: %@" : { + + }, + "only in the right result" : { + + }, + "Only in the right result, not compared: %@" : { + }, "only in the source" : { "localizations" : { @@ -126140,6 +125700,16 @@ } } } + }, + "Only Left" : { + + }, + "Only PL/pgSQL functions and procedures can be debugged." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "Only Right" : { + }, "Only the first %1$lld nodes were loaded, so this value was not searched in full. Switch to %2$@ to search all of it." : { "localizations" : { @@ -126306,6 +125876,9 @@ } } } + }, + "open" : { + }, "Open" : { "localizations" : { @@ -127415,6 +126988,9 @@ }, "Open in Query Editor takes up to %@ rows." : { + }, + "Open in Query Tab" : { + }, "Open in Window" : { "localizations" : { @@ -128441,6 +128017,9 @@ } } } + }, + "Opens a database connection in TablePro" : { + }, "Opens an AWS Systems Manager port forwarding session through a target instance." : { "localizations" : { @@ -129497,6 +129076,9 @@ } } } + }, + "Other rows hold exactly the same values, so the change cannot reach this row alone." : { + }, "Other sessions keep reading the view. Slower, because the new rows are compared with the old ones." : { "localizations" : { @@ -129920,12 +129502,6 @@ } } } - }, - "Owned by" : { - - }, - "Owned by %@" : { - }, "output_path must end in .%@ for this format." : { "localizations" : { @@ -130066,6 +129642,12 @@ } } } + }, + "Owned by" : { + + }, + "Owned by %@" : { + }, "Owned objects" : { "localizations" : { @@ -133260,6 +132842,12 @@ } } } + }, + "Pick the result to compare this one with." : { + + }, + "Pick the tables to join. Their foreign keys become the join conditions." : { + }, "Pick the type of database you want to connect to." : { "localizations" : { @@ -134443,6 +134031,7 @@ } }, "Plugin requires app version %@ or later, but current version is %@" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -134969,6 +134558,9 @@ } } } + }, + "Position" : { + }, "Position in the parent's declared order" : { "extractionState" : "manual", @@ -135242,6 +134834,7 @@ }, "Pre-connect script timed out after 10 seconds" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -135282,6 +134875,7 @@ } }, "Pre-connect script was cancelled" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -136932,6 +136526,7 @@ } }, "Priority %d" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -138488,6 +138083,9 @@ } } } + }, + "Provide a JSON object or an array of objects." : { + }, "Providers" : { "localizations" : { @@ -139462,6 +139060,9 @@ } } } + }, + "query" : { + }, "Query" : { "localizations" : { @@ -142616,9 +142217,6 @@ }, "Read the live server panels TablePro shows: active sessions, server metrics, and slow queries. Available on every engine that has a Server Dashboard in TablePro." : { - }, - "Read the live server panels TablePro shows: active sessions, server metrics, and slow queries. Available on PostgreSQL, MySQL, SQL Server, ClickHouse, and Typesense." : { - }, "Read the live server panels TablePro shows: active sessions, server metrics, and slow queries. Available on PostgreSQL, MySQL, SQL Server, ClickHouse, DuckDB, and SQLite." : { "extractionState" : "stale", @@ -144471,6 +144069,9 @@ }, "Referenced Value" : { + }, + "References" : { + }, "references %@" : { "localizations" : { @@ -144819,6 +144420,9 @@ } } } + }, + "Refuse NULL" : { + }, "Refuse the statement unless it only reads, even where the connection allows writes." : { @@ -145676,6 +145280,9 @@ } } } + }, + "Remove “%@”" : { + }, "Remove “%@”?" : { "localizations" : { @@ -145839,6 +145446,9 @@ } } } + }, + "Remove Attached Database" : { + }, "Remove attachment" : { "localizations" : { @@ -146959,6 +146569,12 @@ } } } + }, + "Remove the comment" : { + + }, + "Remove the default" : { + }, "Remove the extensions from this connection in Edit Connection." : { "extractionState" : "manual", @@ -146986,6 +146602,9 @@ } } } + }, + "Remove this change from the queue" : { + }, "Remove this rule" : { "localizations" : { @@ -147307,6 +146926,9 @@ } } } + }, + "Rename field “%1$@” to “%2$@”" : { + }, "Rename Folder" : { "localizations" : { @@ -147317,9 +146939,23 @@ } } } + }, + "Rename Label" : { + + }, + "Rename label “%1$@” to “%2$@”" : { + + }, + "Rename the selected label" : { + + }, + "Rename to “%@”" : { + + }, + "Rename Type" : { + }, "Rename…" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -147933,6 +147569,7 @@ } }, "Replace…" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -149010,13 +148647,14 @@ } } }, - "Resolve the conflict in a Git client before committing this file." : { + "Resident Memory" : { }, - "Resident Memory" : { + "Resolve the conflict in a Git client before committing this file." : { }, "Resolved on every connect. It stays on this Mac and never syncs, because it can name a command to run." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -150310,6 +149948,9 @@ } } } + }, + "Result %1$d: %2$@" : { + }, "Result %d" : { "localizations" : { @@ -150390,6 +150031,12 @@ } } } + }, + "Result Closed" : { + + }, + "Result Format" : { + }, "Result Options" : { "localizations" : { @@ -151400,6 +151047,9 @@ }, "Rewrites every text column to this character set. TEXT can grow to MEDIUMTEXT, and the table is copied while it runs." : { + }, + "Right" : { + }, "right curly bracket" : { "extractionState" : "manual", @@ -151523,6 +151173,9 @@ } } } + }, + "right value, differs" : { + }, "Right-click a cell and choose Highlight to color rows by value." : { "localizations" : { @@ -151986,6 +151639,9 @@ } } } + }, + "row" : { + }, "Row" : { "localizations" : { @@ -152026,6 +151682,9 @@ } } } + }, + "Row (JSON or CSV)" : { + }, "Row %@ already exists. Edit that row, or give the new one another key." : { "extractionState" : "manual", @@ -152630,6 +152289,9 @@ } } } + }, + "Row Limit" : { + }, "Row no longer exists" : { "localizations" : { @@ -152844,6 +152506,9 @@ } } } + }, + "rows" : { + }, "Rows" : { "localizations" : { @@ -152884,6 +152549,9 @@ } } } + }, + "Rows (JSON or CSV)" : { + }, "Rows %lld-%lld" : { "localizations" : { @@ -152985,9 +152653,6 @@ } } } - }, - "Rows in %1$@ break the foreign key %2$@." : { - }, "Rows in %@ changed after they were compared. Compare again before generating the script." : { "localizations" : { @@ -153001,6 +152666,9 @@ }, "Rows in %@ may block the delete (not counted)" : { + }, + "Rows in %1$@ break the foreign key %2$@." : { + }, "Rows in this export" : { "localizations" : { @@ -153519,6 +153187,9 @@ } } } + }, + "Rows with the same values in these columns are compared. With none chosen, whole rows are matched." : { + }, "Rows, each an array aligned with columns" : { "extractionState" : "manual", @@ -153705,6 +153376,9 @@ } } } + }, + "Run ${query} on ${connection}" : { + }, "Run a maintenance operation the engine supports, on one table or on the whole database. TablePro generates the statements; the user approves them. Needs tools:write." : { "localizations" : { @@ -153748,6 +153422,9 @@ }, "Run a saved query on the Server Dashboard" : { + }, + "Run a SQL statement on a saved connection and pass the result to the next action." : { + }, "Run All Statements" : { "localizations" : { @@ -153758,6 +153435,9 @@ } } } + }, + "Run another query in this tab, or pin a result in another tab." : { + }, "Run button beside each statement" : { "localizations" : { @@ -154080,6 +153760,9 @@ } } } + }, + "Run Query" : { + }, "Run Script" : { "localizations" : { @@ -156159,6 +155842,9 @@ } } } + }, + "Save Type Changes" : { + }, "Save…" : { "localizations" : { @@ -159326,6 +159012,9 @@ } } } + }, + "Second value" : { + }, "Second value is required for BETWEEN" : { "localizations" : { @@ -159590,6 +159279,9 @@ }, "Secret ID File" : { + }, + "Section" : { + }, "Section Header" : { "extractionState" : "stale", @@ -159723,6 +159415,9 @@ }, "Seed %@ made these rows. The next run starts from a new seed." : { + }, + "select" : { + }, "Select" : { "localizations" : { @@ -162810,9 +162505,6 @@ } } } - }, - "Set Next Value…" : { - }, "Set as Active" : { "localizations" : { @@ -163016,6 +162708,12 @@ }, "Set in connections.json." : { + }, + "Set Next Value" : { + + }, + "Set Next Value…" : { + }, "Set NULL" : { "localizations" : { @@ -163099,6 +162797,9 @@ } } } + }, + "Set the comment" : { + }, "Set the connection's username to the IAM principal (a user email, or `name@project.iam` for a service account). The database password is not used." : { "localizations" : { @@ -163139,6 +162840,9 @@ } } } + }, + "Set the default to %@" : { + }, "Set the Microsoft Entra ID application (client) ID for this connection." : { "extractionState" : "manual", @@ -163162,6 +162866,7 @@ }, "Set to NULL" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -163699,6 +163404,7 @@ } }, "Shell script to run before connecting. Non-zero exit aborts connection." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -163821,6 +163527,15 @@ } } } + }, + "Shortcuts" : { + + }, + "Shortcuts wants to add %1$lld rows to \"%2$@\"" : { + + }, + "Shortcuts wants to add a row to \"%@\"" : { + }, "Shortest" : { @@ -165498,6 +165213,9 @@ } } } + }, + "Show Structure of %@" : { + }, "Show system databases and schemas" : { "localizations" : { @@ -165857,12 +165575,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld lignes affichées" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -168813,6 +168525,9 @@ } } } + }, + "Some rows are no longer in the table, so their values could not be read." : { + }, "Some rows couldn't be counted, so more rows may change than shown." : { @@ -169283,6 +168998,7 @@ } }, "Sorted ascending" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -169327,6 +169043,7 @@ "generatesSymbol" : false }, "Sorted descending" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -172074,6 +171791,9 @@ } } } + }, + "Start" : { + }, "Start one to ask about this connection." : { "localizations" : { @@ -172477,6 +172197,9 @@ } } } + }, + "Statement" : { + }, "Statement %1$d of %2$d failed: %3$@" : { "extractionState" : "manual", @@ -174298,7 +174021,6 @@ "generatesSymbol" : false }, "Stopping…" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -174973,6 +174695,9 @@ } } } + }, + "Subtype" : { + }, "Succeeded" : { "localizations" : { @@ -176506,11 +176231,9 @@ } } } - }, - "Sync Delayed" : { - }, "Sync Error" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -176767,6 +176490,7 @@ } }, "Synced" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -176853,6 +176577,7 @@ } }, "Syncing with iCloud…" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -177851,6 +177576,9 @@ }, "Table Created with Errors" : { + }, + "Table Data" : { + }, "Table DDL" : { "extractionState" : "manual", @@ -179110,7 +178838,6 @@ }, "TablePro" : { - "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -179150,6 +178877,42 @@ } } }, + "TablePro and the SSH server could not agree on how to encrypt the connection." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro and the SSH server have no cipher in common." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro and the SSH server have no key exchange or host key method in common." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro and the SSH server have no MAC algorithm in common." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro and the SSH server have no signature algorithm in common for this key." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro can't pass any value of type %2$@ to %1$@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro can't read %@. Check that this account can see its source." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro can't read this unit's source." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro can't turn text into a %2$@ value for %1$@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "TablePro cannot sign an RDS token for \"%@\". Enter the RDS Endpoint (for example mydb.abc123.us-east-1.rds.amazonaws.com:5432) when you connect through a port forward or bastion." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -179907,6 +179670,46 @@ } } }, + "TablePro is free for everyone, at work too, and none of it will become paid." : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro est gratuit pour tout le monde, au travail aussi, et rien ne deviendra payant." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro는 업무용을 포함해 누구에게나 무료이며, 어떤 기능도 유료로 바뀌지 않습니다." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro iş yerinde de dahil herkes için ücretsizdir ve hiçbir kısmı ücretli olmayacak." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro miễn phí cho mọi người, kể cả khi dùng cho công việc, và không phần nào sẽ chuyển sang trả phí." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro 对所有人免费,工作中使用也一样,而且任何部分都不会变成付费。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro 對所有人免費,工作中使用也一樣,而且任何部分都不會變成付費。" + } + } + } + }, "TablePro is reading or writing rows on this connection with settings of its own." : { "extractionState" : "manual", "generatesSymbol" : false @@ -180055,6 +179858,10 @@ } } }, + "TablePro signs RSA keys with rsa-sha2-256 or rsa-sha2-512, not with ssh-rsa, which uses SHA-1." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "TablePro supports Oracle Database 11.1 and later. This server reports an older release (10g or earlier)." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -180067,6 +179874,18 @@ } } }, + "TablePro supports these ciphers: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro supports these key exchange methods: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "TablePro supports these MAC algorithms: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "TablePro takes no funding and sells no data. If you want to do more, sponsorship goes to the same place." : { "localizations" : { "fr" : { @@ -182313,6 +182132,9 @@ } } } + }, + "That connection no longer exists in TablePro." : { + }, "That destination is not valid for this engine." : { "extractionState" : "stale", @@ -182687,6 +182509,9 @@ } } } + }, + "That result is no longer open. Choose another one." : { + }, "That tab is not open." : { "localizations" : { @@ -183278,7 +183103,48 @@ } } }, + "The app stays free, all of it, at work too. If TablePro is part of your work, please buy a license. It is a request, not a condition, and it pays for the next release." : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "L’app reste gratuite, dans son intégralité, au travail aussi. Si TablePro fait partie de votre travail, merci d’acheter une licence. C’est une demande, pas une condition, et elle finance la prochaine version." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "앱 전체가 업무용을 포함해 계속 무료입니다. TablePro가 업무의 일부라면 라이선스를 구매해 주세요. 조건이 아니라 부탁이며, 라이선스가 다음 릴리스를 만듭니다." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulamanın tamamı, iş yerinde de ücretsiz kalır. TablePro işinizin bir parçasıysa lütfen bir lisans satın alın. Bu bir koşul değil, bir rica; bir sonraki sürümü de lisanslar finanse ediyor." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toàn bộ ứng dụng vẫn miễn phí, kể cả khi dùng cho công việc. Nếu TablePro là một phần công việc của bạn, hãy mua một giấy phép. Đây là lời đề nghị, không phải điều kiện, và nó chi trả cho bản phát hành tiếp theo." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "整个 App 一直免费,工作中使用也一样。如果 TablePro 是你工作的一部分,请购买许可证。这是请求,不是条件,它支付了下一个版本的开发。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "整個 App 一直免費,工作中使用也一樣。如果 TablePro 是你工作的一部分,請購買授權。這是請求,不是條件,它支付了下一個版本的開發。" + } + } + } + }, "The app stays free, all of it. If TablePro is useful to you, a license is what pays for the next release." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -183331,6 +183197,14 @@ } } }, + "The attached database \"%@\" has no file." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "The attached file \"%@\" has no name." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "The authenticity of host '%@' can't be established.\n\n%@ key fingerprint is:\n%@\n\nAre you sure you want to continue connecting?" : { "localizations" : { "en" : { @@ -183839,6 +183713,9 @@ } } } + }, + "The cache must be at least 1." : { + }, "The catalog location is not a folder: %@" : { "localizations" : { @@ -183917,6 +183794,12 @@ }, "The changes were edited while the save was checking other tables. Save again to include them." : { + }, + "The changes were saved, but the type could not be read back. Reload before editing it again." : { + + }, + "The changes you have not saved will be lost." : { + }, "The chat and Agent mode keep the active provider." : { @@ -184004,12 +183887,13 @@ "The CHECK on column %@ names column %@, which this save renames. Save the rename on its own first." : { "extractionState" : "manual", "generatesSymbol" : false - }, - "The cache must be at least 1." : { - }, "The chosen theme is not on this Mac, so %@ is used." : { + }, + "The client certificate and key could not be written to a temporary file." : { + "extractionState" : "manual", + "generatesSymbol" : false }, "The client certificate at %@ could not be read as a PEM or DER certificate." : { "extractionState" : "manual", @@ -184549,6 +184433,9 @@ } } } + }, + "The column “%1$@” is no longer in “%2$@”." : { + }, "The column keeps %1$lld of its %2$lld digits after the point, so a longer fraction is rounded." : { "localizations" : { @@ -185326,6 +185213,9 @@ } } } + }, + "The CSV data has no header row." : { + }, "The Dameng connection is closed." : { "extractionState" : "manual", @@ -185390,6 +185280,12 @@ "The data changed after it was read." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "The data could not be read: %@" : { + + }, + "The data has no values to insert into %@." : { + }, "The database “%@” has an open transaction. Closing rolls it back and discards its uncommitted changes." : { "localizations" : { @@ -185802,6 +185698,7 @@ }, "The diagram could not be converted to a PNG image." : { "comment" : "Error message when the ER diagram could not be exported as PNG.", + "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "fr" : { @@ -185844,6 +185741,9 @@ }, "The diagram could not be rendered as %@." : { + }, + "The diagram is saving its changes. Make this change once the save has finished." : { + }, "The disconnect was cancelled." : { "localizations" : { @@ -186530,6 +186430,9 @@ } } } + }, + "The first column of the first row" : { + }, "The first sheet has no header row to map columns from." : { "extractionState" : "manual", @@ -186679,6 +186582,9 @@ } } } + }, + "The foreign key “%1$@” is no longer on “%2$@”." : { + }, "The form produced no statements to run." : { "localizations" : { @@ -186778,6 +186684,9 @@ } } } + }, + "The increment cannot be zero." : { + }, "The index %@ is left out" : { "localizations" : { @@ -186813,9 +186722,6 @@ }, "The installed %1$@ does not support %2$@. Update it in Settings > Extensions." : { - }, - "The installed %1$@ does not support %2$@. Update it in Settings > Plugins." : { - }, "The instance connection name must be in the form project:region:instance." : { "localizations" : { @@ -186883,6 +186789,9 @@ } } } + }, + "The JSON array must contain only objects." : { + }, "The JSON is nested more deeply than DynamoDB allows" : { "extractionState" : "manual", @@ -187032,6 +186941,9 @@ } } } + }, + "The left result stopped at the row cap and is compared on the %@ rows it holds. A row past the cap can show as only on the right." : { + }, "The license has been suspended." : { "localizations" : { @@ -187703,6 +187615,9 @@ } } } + }, + "The minimum must be less than the maximum." : { + }, "The MongoDB client library will not insert a field whose name is empty. Remove the field, then set it on the saved document." : { "extractionState" : "manual", @@ -187727,6 +187642,9 @@ } } } + }, + "The name a statement uses for this database, as in name.table." : { + }, "The name of a DIRECTORY object, not a path. The DBA who created it chose where it points." : { "extractionState" : "stale", @@ -189316,6 +189234,9 @@ } } } + }, + "The request timed out." : { + }, "The request to BigQuery timed out." : { "extractionState" : "manual", @@ -189388,6 +189309,9 @@ } } } + }, + "The right result stopped at the row cap and is compared on the %@ rows it holds. A row past the cap can show as only on the left." : { + }, "The rollback did not undo everything. A statement in the file, such as COMMIT, CREATE, ALTER or DROP, committed the import's transaction as it ran, so some of the statements before the failure were kept." : { @@ -189418,6 +189342,9 @@ "The routine was stopped, and its uncommitted changes were rolled back." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "The row could not be looked up: %@" : { + }, "The row filter on %@ was not a single expression, so every row was exported." : { "extractionState" : "manual", @@ -189443,6 +189370,14 @@ "extractionState" : "manual", "generatesSymbol" : false }, + "The row has no loaded values to be found by." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "The row's loaded values are missing, so it cannot be found again." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "The row's primary key is not loaded, so the row cannot be found." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -189601,9 +189536,6 @@ } } } - }, - "The minimum must be less than the maximum." : { - }, "The save also truncated or dropped a table." : { "localizations" : { @@ -189783,6 +189715,9 @@ } } } + }, + "The saved list could not be read. Adding a database replaces it." : { + }, "The saved list could not be read. Adding an extension replaces it." : { "localizations" : { @@ -189794,6 +189729,10 @@ } } }, + "The saved list of attached databases is not in a format TablePro can read." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "The saved Microsoft Entra ID sign-in expired. Sign in again." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -189903,6 +189842,9 @@ } } } + }, + "The schema changes staged in this diagram will be lost." : { + }, "The script already ran. Compare again to see where the target stands." : { "localizations" : { @@ -190346,6 +190288,22 @@ } } }, + "The server offers only ciphers TablePro no longer supports: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "The server offers only host key types TablePro no longer supports: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "The server offers only key exchange methods TablePro no longer supports: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "The server offers only MAC algorithms TablePro no longer supports: %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "The server redirected the request to %@. TablePro does not follow redirects, and the coordinator never sends one, so a proxy in front of it did. Check the host, port and SSL Mode." : { "extractionState" : "manual", "generatesSymbol" : false @@ -190373,6 +190331,12 @@ } } } + }, + "The server refused a change, so nothing was saved." : { + + }, + "The server refused a change. The changes before it were saved." : { + }, "The server rejected TablePro's credential." : { "localizations" : { @@ -190442,6 +190406,10 @@ } } }, + "The server runs %@." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "The server sent an unexpected message and the connection was reset. Run the query again." : { "extractionState" : "manual", "generatesSymbol" : false, @@ -191139,6 +191107,9 @@ } } } + }, + "The SSH server closed the connection during the handshake." : { + }, "The SSH server closed the connection while the agent's keys were being offered. Servers accept a limited number of tries, 6 by default, and an agent holding more keys than that runs out before the right one. Name the key with IdentityFile and IdentitiesOnly yes for this host in ~/.ssh/config, or set Identity File on this connection." : { "localizations" : { @@ -191331,6 +191302,9 @@ } } } + }, + "The SSH server ended the connection during the handshake: %@" : { + }, "The SSH server is asking for additional verification." : { "localizations" : { @@ -191547,6 +191521,7 @@ } }, "The statement that was sent, with its prefix" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -191679,6 +191654,10 @@ } } }, + "The structure of a table in an attached database cannot be changed here." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "The structure of the tables this query uses is read when the request is sent." : { "localizations" : { "fr" : { @@ -192424,6 +192403,9 @@ } } } + }, + "The text encoding is invalid." : { + }, "The text is not valid JSON." : { "extractionState" : "manual", @@ -192788,6 +192770,12 @@ } } } + }, + "The two results share no column name, so there is nothing to compare." : { + + }, + "The type changed on the server since it was read. Reload it, then save again." : { + }, "The type's shape, such as enum, composite, domain, range, aliasType, tableType or clrType" : { "localizations" : { @@ -193769,6 +193757,10 @@ } } }, + "This _id reads as more than one value type in this collection, such as text and an ObjectId, so TablePro cannot tell which document the row names. Use a query." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, "This account signs in with Kerberos. Set Authentication to Kerberos (GSSAPI), then reconnect." : { "extractionState" : "manual", "generatesSymbol" : false @@ -193970,6 +193962,15 @@ "This call matches more than one overload of %@, so Oracle can't run it (PLS-00307)." : { "extractionState" : "manual", "generatesSymbol" : false + }, + "This cannot be undone. The drop fails while a column or other object still uses it." : { + + }, + "This cannot be undone. Without Cascade, the drop fails while a column, function or other type still uses it." : { + + }, + "This change does not apply to this kind of type." : { + }, "This change needs %@ to be recreated, and it cannot be recreated from the definition the database stored for it. Virtual tables, such as full-text search tables, are the usual reason." : { "localizations" : { @@ -194793,6 +194794,9 @@ } } } + }, + "This connection's startup commands leave a transaction open, so rows added from Shortcuts could not be committed." : { + }, "This conversation is too long to continue. Start a new conversation and send your message there." : { "localizations" : { @@ -194964,6 +194968,9 @@ } } } + }, + "This database cannot make the change “%@”." : { + }, "This database cannot reference a table in another schema." : { "localizations" : { @@ -195310,6 +195317,9 @@ } } } + }, + "This database reports no tables to join." : { + }, "This database reports no tables to search." : { @@ -198163,6 +198173,7 @@ } }, "This plugin requires TablePro %@ or later" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -198567,6 +198578,16 @@ } } } + }, + "This sequence's settings could not be read, so they cannot be changed here." : { + + }, + "This server can't show the default value of %@, so enter a value for it." : { + "extractionState" : "manual", + "generatesSymbol" : false + }, + "This server cannot rename a label." : { + }, "This server did not identify as Databend. Check the host and port of its MySQL handler." : { "extractionState" : "manual", @@ -199013,6 +199034,9 @@ } } } + }, + "This Tab" : { + }, "This tab is on %@. Switch the connection to that database to run it." : { "localizations" : { @@ -201544,6 +201568,9 @@ } } } + }, + "Too many rows. Add up to %lld rows at a time." : { + }, "Tool" : { "localizations" : { @@ -203441,6 +203468,7 @@ }, "Try again, or install the plugin from Settings > Plugins." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -203609,6 +203637,9 @@ } } } + }, + "Turn off to add the check now and validate the stored values later." : { + }, "Turn on Apple Intelligence in System Settings > Apple Intelligence & Siri." : { @@ -203771,6 +203802,7 @@ }, "Type %@ in the query to set where the cursor lands after keyword expansion." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -206655,9 +206687,6 @@ } } } - }, - "Update Needed" : { - }, "Update Now" : { "localizations" : { @@ -206703,6 +206732,7 @@ }, "Update or reinstall the plugin in Settings > Plugins." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -206888,6 +206918,7 @@ }, "Update the database driver in Settings > Plugins to run a batch more than once with GO." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -206898,6 +206929,7 @@ } }, "Update the database driver in Settings > Plugins to run several statements in one call." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -209108,6 +209140,15 @@ } } } + }, + "Validate" : { + + }, + "Validate constraint “%@”" : { + + }, + "Validated on save" : { + }, "Validation Failed" : { "localizations" : { @@ -209819,12 +209860,6 @@ "fr" : { "variations" : { "plural" : { - "many" : { - "stringUnit" : { - "state" : "translated", - "value" : "Valeurs de %lld lignes chargées" - } - }, "one" : { "stringUnit" : { "state" : "translated", @@ -211120,6 +211155,7 @@ } }, "Vim mode" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -211448,6 +211484,12 @@ }, "Warnings: %d" : { + }, + "Was “%@”" : { + + }, + "Was %@" : { + }, "Watch shared folders for connection files." : { "localizations" : { @@ -213518,6 +213560,9 @@ } } } + }, + "Whole row" : { + }, "Whole Words" : { "localizations" : { @@ -215040,6 +215085,7 @@ } }, "You have unsaved changes to the table structure. Refreshing will discard these changes." : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -215079,6 +215125,46 @@ } } }, + "You've used TablePro on %lld days" : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vous avez utilisé TablePro pendant %lld jours" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro를 %lld일 동안 사용하셨습니다" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro'yu %lld gün kullandınız" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn đã dùng TablePro được %lld ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你已经使用 TablePro %lld 天了" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你已經使用 TablePro %lld 天了" + } + } + } + }, "Your account is not allowed to read the source of %@" : { "extractionState" : "manual", "generatesSymbol" : false, diff --git a/apple/TablePro/Views/Main/Extensions/MainContentCoordinator+Registry.swift b/apple/TablePro/Views/Main/Extensions/MainContentCoordinator+Registry.swift index b9f7e9bfc0..7c88e6920e 100644 --- a/apple/TablePro/Views/Main/Extensions/MainContentCoordinator+Registry.swift +++ b/apple/TablePro/Views/Main/Extensions/MainContentCoordinator+Registry.swift @@ -40,6 +40,10 @@ extension MainContentCoordinator { .contains { $0.hasAnyUnsavedWork() } } + static var isAnyQueryRunning: Bool { + activeCoordinators.values.contains { $0.tabExecution.isAnyExecuting } + } + static func hasRunningQuery(forConnection connectionId: UUID) -> Bool { activeCoordinators.values .filter { $0.connectionId == connectionId } diff --git a/apple/TablePro/Views/Support/SupportAskSheetPresenter.swift b/apple/TablePro/Views/Support/SupportAskSheetPresenter.swift new file mode 100644 index 0000000000..733123e62f --- /dev/null +++ b/apple/TablePro/Views/Support/SupportAskSheetPresenter.swift @@ -0,0 +1,84 @@ +// +// SupportAskSheetPresenter.swift +// TablePro +// + +import AppKit +import SwiftUI + +@MainActor +internal protocol SupportAskPresenting: AnyObject { + var isPresenting: Bool { get } + + /// Attaches the ask to `parent`. Returns false, and calls nothing, when it cannot. + func present(usageDays: Int, on parent: NSWindow, answer: @escaping (SupportAskAnswer) -> Void) -> Bool +} + +/// Runs the ask as a sheet on the window in use, the shape `ExportSheetPresenter` uses. +/// +/// Every way the sheet can go reports exactly one answer: a button, Escape, which is Not Now, or +/// the window closing under it, which counts as Not Now too. +@MainActor +internal final class SupportAskSheetPresenter: SupportAskPresenting { + private var sheetWindow: NSWindow? + private weak var parentWindow: NSWindow? + private var report: ((SupportAskAnswer) -> Void)? + private var parentCloseObserver: NSObjectProtocol? + + internal init() {} + + internal var isPresenting: Bool { + sheetWindow != nil + } + + internal func present( + usageDays: Int, + on parent: NSWindow, + answer: @escaping (SupportAskAnswer) -> Void + ) -> Bool { + guard sheetWindow == nil, parent.attachedSheet == nil else { return false } + + let view = SupportAskView(usageDays: usageDays) { [weak self] choice in + self?.finish(with: choice) + } + let hosting = NSHostingController(rootView: view) + hosting.sizingOptions = [.preferredContentSize] + let sheet = NSWindow(contentViewController: hosting) + sheet.isReleasedWhenClosed = false + + sheetWindow = sheet + parentWindow = parent + report = answer + parentCloseObserver = NotificationCenter.default.addObserver( + forName: NSWindow.willCloseNotification, + object: parent, + queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { self?.finish(with: .notNow) } + } + parent.beginSheet(sheet) + return true + } + + private func finish(with choice: SupportAskAnswer) { + guard let sheet = sheetWindow else { return } + let reportAnswer = report + let parent = parentWindow + + sheetWindow = nil + parentWindow = nil + report = nil + if let parentCloseObserver { + NotificationCenter.default.removeObserver(parentCloseObserver) + } + parentCloseObserver = nil + + if let parent, parent.attachedSheet === sheet { + parent.endSheet(sheet) + } else { + sheet.orderOut(nil) + } + sheet.contentViewController = nil + reportAnswer?(choice) + } +} diff --git a/apple/TablePro/Views/Support/SupportAskView.swift b/apple/TablePro/Views/Support/SupportAskView.swift new file mode 100644 index 0000000000..c8c58d3f53 --- /dev/null +++ b/apple/TablePro/Views/Support/SupportAskView.swift @@ -0,0 +1,64 @@ +// +// SupportAskView.swift +// TablePro +// + +import AppKit +import SwiftUI + +/// The license ask, shown as a sheet once someone has used TablePro on 30 days. +/// +/// It says what the Support window says, once, and takes any answer: buying, saying TablePro is +/// for personal use, which quiets it for a year, or not now. Nothing is withheld whichever is +/// chosen. +struct SupportAskView: View { + let usageDays: Int + let answer: (SupportAskAnswer) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + HStack(alignment: .top, spacing: 16) { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .scaledToFit() + .frame(width: 64, height: 64) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: 10) { + Text("You've used TablePro on \(usageDays) days") + .font(.headline) + + Text("TablePro is free for everyone, at work too, and none of it will become paid.") + .fixedSize(horizontal: false, vertical: true) + + Text( + "If TablePro is part of your work now, please buy a license. Licenses are what pay for the next release." + ) + .fixedSize(horizontal: false, vertical: true) + } + } + + HStack(spacing: 12) { + Button("I Use It Personally") { answer(.personal) } + .accessibilityIdentifier("support-ask-personal") + + Spacer() + + Button("Not Now") { answer(.notNow) } + .keyboardShortcut(.cancelAction) + .accessibilityIdentifier("support-ask-not-now") + + Button("Buy a License") { answer(.buy) } + .keyboardShortcut(.defaultAction) + .accessibilityIdentifier("support-ask-buy") + } + } + .padding(20) + .frame(width: 480) + .fixedSize(horizontal: false, vertical: true) + } +} + +#Preview { + SupportAskView(usageDays: 30) { _ in } +} diff --git a/apple/TablePro/Views/Support/SupportView.swift b/apple/TablePro/Views/Support/SupportView.swift index dfdb2288af..97bb257a44 100644 --- a/apple/TablePro/Views/Support/SupportView.swift +++ b/apple/TablePro/Views/Support/SupportView.swift @@ -63,8 +63,9 @@ struct SupportView: View { ), String( localized: """ - The app stays free, all of it. If TablePro is useful to you, a license is what \ - pays for the next release. + The app stays free, all of it, at work too. If TablePro is part of your work, \ + please buy a license. It is a request, not a condition, and it pays for the \ + next release. """ ) ] diff --git a/apple/TableProTests/Core/Services/Licensing/SupportLinksTests.swift b/apple/TableProTests/Core/Services/Licensing/SupportLinksTests.swift index b46b758778..caf724177e 100644 --- a/apple/TableProTests/Core/Services/Licensing/SupportLinksTests.swift +++ b/apple/TableProTests/Core/Services/Licensing/SupportLinksTests.swift @@ -12,7 +12,7 @@ import Testing struct SupportLinksTests { private static let everyReferrer: [SupportReferrer] = - [.supportWindow, .aboutPanel, .licenseSettings, .activationSheet] + [.supportWindow, .aboutPanel, .licenseSettings, .activationSheet, .ask] + ProFeature.allCases.map { .featureGate($0) } private static func components(_ url: URL) -> URLComponents? { diff --git a/apple/TableProTests/Core/Services/Support/SupportAskCoordinatorTests.swift b/apple/TableProTests/Core/Services/Support/SupportAskCoordinatorTests.swift new file mode 100644 index 0000000000..373180029a --- /dev/null +++ b/apple/TableProTests/Core/Services/Support/SupportAskCoordinatorTests.swift @@ -0,0 +1,224 @@ +// +// SupportAskCoordinatorTests.swift +// TableProTests +// + +import AppKit +import Foundation +@testable import TablePro +import TableProTestSupport +import Testing + +@MainActor +struct SupportAskCoordinatorTests { + @MainActor + final class RecordingPresenter: SupportAskPresenting { + private(set) var shownDays: [Int] = [] + private var reply: ((SupportAskAnswer) -> Void)? + + var isPresenting: Bool { + reply != nil + } + + func present(usageDays: Int, on parent: NSWindow, answer: @escaping (SupportAskAnswer) -> Void) -> Bool { + shownDays.append(usageDays) + reply = answer + return true + } + + func choose(_ answer: SupportAskAnswer) { + let pending = reply + reply = nil + pending?(answer) + } + } + + @MainActor + final class Harness { + let suite: EphemeralDefaults + let presenter = RecordingPresenter() + let window: NSWindow + let launchedAt = Date(timeIntervalSince1970: 1_793_000_000) + var now: Date + var hasLicense = false + var isQueryRunning = false + private(set) var openedURLs: [URL] = [] + private(set) var coordinator: SupportAskCoordinator! + + init(daysBeforeLaunch: Int, windowIdentifier: String = WindowIdentifier.connection) throws { + suite = try EphemeralDefaults() + now = launchedAt + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt + + // The launch itself counts as one more day of use. + suite.defaults.set(daysBeforeLaunch, forKey: UsageDayCounter.Keys.days) + suite.defaults.set("2026-10-01", forKey: UsageDayCounter.Keys.lastDay) + + window = NSWindow( + contentRect: NSRect(x: -20_000, y: -20_000, width: 320, height: 200), + styleMask: [.titled], + backing: .buffered, + defer: true + ) + window.isReleasedWhenClosed = false + window.identifier = NSUserInterfaceItemIdentifier(windowIdentifier) + window.orderFront(nil) + + let counter = UsageDayCounter(defaults: suite.defaults, calendar: calendar) + coordinator = SupportAskCoordinator( + counter: counter, + defaults: suite.defaults, + presenter: presenter, + environment: .init( + hasLicense: { [unowned self] in hasLicense }, + isQueryRunning: { [unowned self] in isQueryRunning }, + keyWindow: { [unowned self] in window }, + isModalSessionRunning: { false }, + openPricing: { [unowned self] in openedURLs.append($0) }, + now: { [unowned self] in now } + ), + calendar: calendar, + clock: ManualClock() + ) + coordinator.start() + } + + func advance(days: Int, seconds: TimeInterval = 0) { + now = now.addingTimeInterval(TimeInterval(days) * 86_400 + seconds) + } + + func tearDown() { + window.orderOut(nil) + } + } + + @Test("On the 30th day of use, past the launch quiet period, the ask shows on the window in use") + func asksOnTheThirtiethDay() throws { + let harness = try Harness(daysBeforeLaunch: 29) + defer { harness.tearDown() } + + harness.advance(days: 0, seconds: 360) + harness.coordinator.askIfDue() + + #expect(harness.presenter.shownDays == [30]) + #expect(harness.suite.defaults.object(forKey: SupportAskCoordinator.Keys.lastAskedAt) as? Date == harness.now) + } + + @Test("The 29th day of use asks nothing") + func waitsBeforeTheThirtiethDay() throws { + let harness = try Harness(daysBeforeLaunch: 28) + defer { harness.tearDown() } + + harness.advance(days: 0, seconds: 360) + harness.coordinator.askIfDue() + + #expect(harness.presenter.shownDays.isEmpty) + } + + @Test("Inside the launch quiet period, or while a query runs, nothing shows") + func quietMomentsOnly() throws { + let harness = try Harness(daysBeforeLaunch: 29) + defer { harness.tearDown() } + + harness.advance(days: 0, seconds: 60) + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays.isEmpty) + + harness.advance(days: 0, seconds: 600) + harness.isQueryRunning = true + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays.isEmpty) + + harness.isQueryRunning = false + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays == [30]) + } + + @Test("A license holds the ask however many days of use") + func licenseHoldsIt() throws { + let harness = try Harness(daysBeforeLaunch: 200) + defer { harness.tearDown() } + harness.hasLicense = true + + harness.advance(days: 0, seconds: 600) + harness.coordinator.askIfDue() + + #expect(harness.presenter.shownDays.isEmpty) + } + + @Test("A connection window opened after the first, whose identifier carries a suffix, carries the ask") + func suffixedConnectionWindowHostsIt() throws { + let harness = try Harness(daysBeforeLaunch: 29, windowIdentifier: "\(WindowIdentifier.connection)-2") + defer { harness.tearDown() } + + harness.advance(days: 0, seconds: 600) + harness.coordinator.askIfDue() + + #expect(harness.presenter.shownDays == [30]) + } + + @Test("Settings and other utility windows never carry the ask") + func onlyWorkWindowsHostIt() throws { + let harness = try Harness(daysBeforeLaunch: 29, windowIdentifier: WindowIdentifier.settings) + defer { harness.tearDown() } + + harness.advance(days: 0, seconds: 600) + harness.coordinator.askIfDue() + + #expect(harness.presenter.shownDays.isEmpty) + } + + @Test("Buying opens the pricing page tagged app-ask, and the next ask waits 90 days") + func buyOpensPricingAndWaits() throws { + let harness = try Harness(daysBeforeLaunch: 29) + defer { harness.tearDown() } + harness.advance(days: 0, seconds: 600) + harness.coordinator.askIfDue() + + harness.presenter.choose(.buy) + + let opened = try #require(harness.openedURLs.first) + let query = URLComponents(url: opened, resolvingAgainstBaseURL: false)?.queryItems + #expect(query == [URLQueryItem(name: "ref", value: "app-ask")]) + + harness.advance(days: 89) + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays == [30]) + + harness.advance(days: 1) + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays == [30, 30]) + } + + @Test("Personal use quiets the ask for a year") + func personalUseWaitsAYear() throws { + let harness = try Harness(daysBeforeLaunch: 29) + defer { harness.tearDown() } + harness.advance(days: 0, seconds: 600) + harness.coordinator.askIfDue() + + harness.presenter.choose(.personal) + + harness.advance(days: 364) + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays == [30]) + #expect(harness.openedURLs.isEmpty) + + harness.advance(days: 1) + harness.coordinator.askIfDue() + #expect(harness.presenter.shownDays == [30, 30]) + } + + @Test("While the sheet is open, no second ask is attempted") + func oneAskAtATime() throws { + let harness = try Harness(daysBeforeLaunch: 29) + defer { harness.tearDown() } + harness.advance(days: 0, seconds: 600) + + harness.coordinator.askIfDue() + harness.coordinator.askIfDue() + + #expect(harness.presenter.shownDays == [30]) + } +} diff --git a/apple/TableProTests/Core/Services/Support/SupportAskScheduleTests.swift b/apple/TableProTests/Core/Services/Support/SupportAskScheduleTests.swift new file mode 100644 index 0000000000..4ae7df5c74 --- /dev/null +++ b/apple/TableProTests/Core/Services/Support/SupportAskScheduleTests.swift @@ -0,0 +1,77 @@ +// +// SupportAskScheduleTests.swift +// TableProTests +// + +import Foundation +@testable import TablePro +import Testing + +struct SupportAskScheduleTests { + private let calendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt + return calendar + }() + + private let askedAt = Date(timeIntervalSince1970: 1_793_000_000) + + private func day(_ offset: Int) -> Date { + askedAt.addingTimeInterval(TimeInterval(offset) * 86_400) + } + + @Test("Nothing is asked before the 30th day of use") + func waitsForTheThirtiethDay() { + let before = SupportAskSchedule(usageDays: 29, hasLicense: false, lastAskedAt: nil, lastAnswer: nil) + let on = SupportAskSchedule(usageDays: 30, hasLicense: false, lastAskedAt: nil, lastAnswer: nil) + + #expect(!before.isDue(at: askedAt, calendar: calendar)) + #expect(on.isDue(at: askedAt, calendar: calendar)) + } + + @Test("A license closes the ask whatever the count") + func licenseClosesIt() { + let schedule = SupportAskSchedule(usageDays: 400, hasLicense: true, lastAskedAt: nil, lastAnswer: nil) + + #expect(!schedule.isDue(at: askedAt, calendar: calendar)) + } + + @Test( + "After any answer but personal use, the next ask waits 90 days", + arguments: [nil, SupportAskAnswer.buy, .notNow] as [SupportAskAnswer?] + ) + func ninetyDaysAfterAnAsk(answer: SupportAskAnswer?) { + let schedule = SupportAskSchedule(usageDays: 45, hasLicense: false, lastAskedAt: askedAt, lastAnswer: answer) + + #expect(!schedule.isDue(at: day(89), calendar: calendar)) + #expect(schedule.isDue(at: day(90), calendar: calendar)) + } + + @Test("Personal use quiets the ask for a year") + func personalUseWaitsAYear() { + let schedule = SupportAskSchedule(usageDays: 45, hasLicense: false, lastAskedAt: askedAt, lastAnswer: .personal) + + #expect(!schedule.isDue(at: day(90), calendar: calendar)) + #expect(!schedule.isDue(at: day(364), calendar: calendar)) + #expect(schedule.isDue(at: day(365), calendar: calendar)) + } + + @Test("The ask waits out the first five minutes after launch") + func launchQuietPeriod() { + let launchedAt = askedAt + + #expect(!SupportAskSchedule.isQuietMoment( + launchedAt: launchedAt, now: launchedAt.addingTimeInterval(299), isQueryRunning: false + )) + #expect(SupportAskSchedule.isQuietMoment( + launchedAt: launchedAt, now: launchedAt.addingTimeInterval(300), isQueryRunning: false + )) + } + + @Test("A running query holds the ask") + func runningQueryHoldsIt() { + #expect(!SupportAskSchedule.isQuietMoment( + launchedAt: askedAt, now: askedAt.addingTimeInterval(3_600), isQueryRunning: true + )) + } +} diff --git a/apple/TableProTests/Core/Services/Support/UsageDayCounterTests.swift b/apple/TableProTests/Core/Services/Support/UsageDayCounterTests.swift new file mode 100644 index 0000000000..a23cc7fea5 --- /dev/null +++ b/apple/TableProTests/Core/Services/Support/UsageDayCounterTests.swift @@ -0,0 +1,71 @@ +// +// UsageDayCounterTests.swift +// TableProTests +// + +import Foundation +@testable import TablePro +import Testing + +@MainActor +struct UsageDayCounterTests { + /// UTC+7, so a UTC timestamp near midnight lands on a different local day than its UTC date. + private let calendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 7 * 3_600) ?? .gmt + return calendar + }() + + private func date(_ text: String) throws -> Date { + try #require(ISO8601DateFormatter().date(from: text)) + } + + @Test("A day counts once however often the app is used that day") + func sameDayCountsOnce() throws { + let suite = try EphemeralDefaults() + let counter = UsageDayCounter(defaults: suite.defaults, calendar: calendar) + + try counter.recordUse(at: date("2026-11-02T01:00:00Z")) + try counter.recordUse(at: date("2026-11-02T09:30:00Z")) + try counter.recordUse(at: date("2026-11-02T16:59:00Z")) + + #expect(counter.days == 1) + } + + @Test("Each new day of use adds one, however many days were skipped") + func newDaysAdd() throws { + let suite = try EphemeralDefaults() + let counter = UsageDayCounter(defaults: suite.defaults, calendar: calendar) + + try counter.recordUse(at: date("2026-11-02T03:00:00Z")) + try counter.recordUse(at: date("2026-11-03T03:00:00Z")) + try counter.recordUse(at: date("2026-11-20T03:00:00Z")) + + #expect(counter.days == 3) + } + + @Test("The day turns at local midnight, not at UTC midnight") + func dayTurnsAtLocalMidnight() throws { + let suite = try EphemeralDefaults() + let counter = UsageDayCounter(defaults: suite.defaults, calendar: calendar) + + // 23:59 and 00:01 local, both on 2 November in UTC. + try counter.recordUse(at: date("2026-11-02T16:59:00Z")) + try counter.recordUse(at: date("2026-11-02T17:01:00Z")) + + #expect(counter.days == 2) + } + + @Test("The count is read back by a new counter on the same defaults") + func countPersists() throws { + let suite = try EphemeralDefaults() + let first = UsageDayCounter(defaults: suite.defaults, calendar: calendar) + try first.recordUse(at: date("2026-11-02T03:00:00Z")) + try first.recordUse(at: date("2026-11-03T03:00:00Z")) + + let second = UsageDayCounter(defaults: suite.defaults, calendar: calendar) + try second.recordUse(at: date("2026-11-03T08:00:00Z")) + + #expect(second.days == 2) + } +} From 19df5be4f7ff2a4c69ae9208ac59ebf7ccda41aa Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 31 Oct 2026 01:33:33 +0700 Subject: [PATCH 2/2] docs(changelog): note the 30-day license ask --- apple/changelog.d/3922.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 apple/changelog.d/3922.added.md diff --git a/apple/changelog.d/3922.added.md b/apple/changelog.d/3922.added.md new file mode 100644 index 0000000000..aa1b7aa7e4 --- /dev/null +++ b/apple/changelog.d/3922.added.md @@ -0,0 +1 @@ +After 30 days of use without a license, a sheet asks once whether you would buy one, then at most every 90 days.