From fb4c99d5f8c4e36e261c6ff68c15e774831986a2 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 17:31:04 +0700 Subject: [PATCH 1/4] feat: add etcd v3 database plugin with prefix-tree key browsing Add a new etcd driver plugin using the v3 HTTP/JSON gateway (no C bridge). Supports prefix-tree sidebar navigation, etcdctl-compatible editor syntax, lease management, bounded watch, mTLS, auth/RBAC, and cluster info. Also fixes QueryTab.buildBaseTableQuery to use plugin's buildBrowseQuery when available, so NoSQL plugins get table-specific initial queries instead of hardcoded SCAN commands. --- CHANGELOG.md | 1 + EtcdDriverPlugin/EtcdCommandParser.swift | 508 + EtcdDriverPlugin/EtcdHttpClient.swift | 1045 + EtcdDriverPlugin/EtcdPlugin.swift | 118 + EtcdDriverPlugin/EtcdPluginDriver.swift | 1000 + EtcdDriverPlugin/EtcdQueryBuilder.swift | 202 + EtcdDriverPlugin/EtcdStatementGenerator.swift | 153 + .../EtcdDriverPlugin/EtcdCommandParser.swift | 508 + Plugins/EtcdDriverPlugin/EtcdHttpClient.swift | 1045 + Plugins/EtcdDriverPlugin/EtcdPlugin.swift | 118 + .../EtcdDriverPlugin/EtcdPluginDriver.swift | 1000 + .../EtcdDriverPlugin/EtcdQueryBuilder.swift | 202 + .../EtcdStatementGenerator.swift | 153 + Plugins/EtcdDriverPlugin/Info.plist | 8 + TablePro.xcodeproj/project.pbxproj | 162 + TablePro/Core/Plugins/PluginManager.swift | 12 + ...ginMetadataRegistry+RegistryDefaults.swift | 108 + TablePro/Core/SSH/LibSSH2Tunnel.swift | 2 +- TablePro/Core/SSH/LibSSH2TunnelFactory.swift | 2 +- .../Connection/DatabaseConnection.swift | 4 +- TablePro/Models/Query/QueryTab.swift | 9 + TablePro/Resources/Localizable.xcstrings | 25274 ++++++++-------- 22 files changed, 19176 insertions(+), 12458 deletions(-) create mode 100644 EtcdDriverPlugin/EtcdCommandParser.swift create mode 100644 EtcdDriverPlugin/EtcdHttpClient.swift create mode 100644 EtcdDriverPlugin/EtcdPlugin.swift create mode 100644 EtcdDriverPlugin/EtcdPluginDriver.swift create mode 100644 EtcdDriverPlugin/EtcdQueryBuilder.swift create mode 100644 EtcdDriverPlugin/EtcdStatementGenerator.swift create mode 100644 Plugins/EtcdDriverPlugin/EtcdCommandParser.swift create mode 100644 Plugins/EtcdDriverPlugin/EtcdHttpClient.swift create mode 100644 Plugins/EtcdDriverPlugin/EtcdPlugin.swift create mode 100644 Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift create mode 100644 Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift create mode 100644 Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift create mode 100644 Plugins/EtcdDriverPlugin/Info.plist diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb29aea..10e215ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- etcd v3 plugin with prefix-tree key browsing, etcdctl syntax editor, lease management, watch, mTLS, auth, and cluster info - Save Changes button in toolbar for committing pending data edits - Confirmation dialog before deleting a connection - Confirmation dialog before sort, pagination, filter, or search discards unsaved edits diff --git a/EtcdDriverPlugin/EtcdCommandParser.swift b/EtcdDriverPlugin/EtcdCommandParser.swift new file mode 100644 index 00000000..2a3dfe90 --- /dev/null +++ b/EtcdDriverPlugin/EtcdCommandParser.swift @@ -0,0 +1,508 @@ +// +// EtcdCommandParser.swift +// TablePro +// +// Parses etcdctl-compatible command strings into structured operations. +// Supports: get, put, del, watch, lease, member, endpoint, compaction, auth, user, role commands. +// + +import Foundation +import os +import TableProPluginKit + +enum EtcdOperation { + // KV + case get(key: String, prefix: Bool, limit: Int64?, keysOnly: Bool, sortOrder: EtcdSortOrder?, sortTarget: EtcdSortTarget?) + case put(key: String, value: String, leaseId: Int64?) + case del(key: String, prefix: Bool) + case watch(key: String, prefix: Bool, timeout: TimeInterval) + + // Lease + case leaseGrant(ttl: Int64) + case leaseRevoke(leaseId: Int64) + case leaseTimetolive(leaseId: Int64, keys: Bool) + case leaseList + case leaseKeepAlive(leaseId: Int64) + + // Cluster + case memberList + case endpointStatus + case endpointHealth + + // Maintenance + case compaction(revision: Int64, physical: Bool) + + // Auth + case authEnable + case authDisable + case userAdd(name: String, password: String?) + case userDelete(name: String) + case userList + case roleAdd(name: String) + case roleDelete(name: String) + case roleList + case userGrantRole(user: String, role: String) + case userRevokeRole(user: String, role: String) + + // Generic fallback + case unknown(command: String, args: [String]) +} + +enum EtcdSortOrder: String { + case ascend = "ASCEND" + case descend = "DESCEND" +} + +enum EtcdSortTarget: String { + case key = "KEY" + case version = "VERSION" + case createRevision = "CREATE" + case modRevision = "MOD" + case value = "VALUE" +} + +enum EtcdParseError: Error { + case emptySyntax + case unknownCommand(String) + case missingArgument(String) + case invalidArgument(String) +} + +extension EtcdParseError: PluginDriverError { + var pluginErrorMessage: String { + switch self { + case .emptySyntax: return String(localized: "Empty etcd command") + case .unknownCommand(let cmd): return String(localized: "Unknown command: \(cmd)") + case .missingArgument(let msg): return String(localized: "Missing argument: \(msg)") + case .invalidArgument(let msg): return String(localized: "Invalid argument: \(msg)") + } + } +} + +struct EtcdCommandParser { + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdCommandParser") + + // MARK: - Public API + + static func parse(_ input: String) throws -> EtcdOperation { + let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { throw EtcdParseError.emptySyntax } + + let tokens = tokenize(trimmed) + guard let first = tokens.first else { throw EtcdParseError.emptySyntax } + + let command = first.lowercased() + let remaining = Array(tokens.dropFirst()) + + switch command { + case "get": return try parseGet(remaining) + case "put": return try parsePut(remaining) + case "del", "delete": return try parseDel(remaining) + case "watch": return try parseWatch(remaining) + case "lease": return try parseLease(remaining) + case "member": return try parseMember(remaining) + case "endpoint": return try parseEndpoint(remaining) + case "compaction": return try parseCompaction(remaining) + case "auth": return try parseAuth(remaining) + case "user": return try parseUser(remaining) + case "role": return try parseRole(remaining) + default: return .unknown(command: command, args: remaining) + } + } + + // MARK: - KV Commands + + private static func parseGet(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let key = positional.first else { + throw EtcdParseError.missingArgument("get requires a key") + } + + let prefix = flags.has("prefix") + let keysOnly = flags.has("keys-only") + + var limit: Int64? + if let limitStr = flags.value(for: "limit") { + guard let parsed = Int64(limitStr) else { + throw EtcdParseError.invalidArgument("--limit must be an integer") + } + limit = parsed + } + + var sortOrder: EtcdSortOrder? + if let orderStr = flags.value(for: "order") { + guard let parsed = EtcdSortOrder(rawValue: orderStr.uppercased()) else { + throw EtcdParseError.invalidArgument("--order must be ASCEND or DESCEND") + } + sortOrder = parsed + } + + var sortTarget: EtcdSortTarget? + if let sortByStr = flags.value(for: "sort-by") { + guard let parsed = EtcdSortTarget(rawValue: sortByStr.uppercased()) else { + throw EtcdParseError.invalidArgument("--sort-by must be KEY, VERSION, CREATE, MOD, or VALUE") + } + sortTarget = parsed + } + + return .get( + key: key, + prefix: prefix, + limit: limit, + keysOnly: keysOnly, + sortOrder: sortOrder, + sortTarget: sortTarget + ) + } + + private static func parsePut(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard positional.count >= 2 else { + throw EtcdParseError.missingArgument("put requires key and value") + } + + let key = positional[0] + let value = positional[1] + + var leaseId: Int64? + if let leaseStr = flags.value(for: "lease") { + leaseId = try parseLeaseId(leaseStr) + } + + return .put(key: key, value: value, leaseId: leaseId) + } + + private static func parseDel(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let key = positional.first else { + throw EtcdParseError.missingArgument("del requires a key") + } + + let prefix = flags.has("prefix") + return .del(key: key, prefix: prefix) + } + + private static func parseWatch(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let key = positional.first else { + throw EtcdParseError.missingArgument("watch requires a key") + } + + let prefix = flags.has("prefix") + + var timeout: TimeInterval = 30 + if let timeoutStr = flags.value(for: "timeout") { + guard let parsed = TimeInterval(timeoutStr) else { + throw EtcdParseError.invalidArgument("--timeout must be a number") + } + timeout = parsed + } + + return .watch(key: key, prefix: prefix, timeout: timeout) + } + + // MARK: - Lease Commands + + private static func parseLease(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("lease requires a subcommand (grant, revoke, timetolive, list, keep-alive)") + } + + let args = Array(tokens.dropFirst()) + + switch subcommand.lowercased() { + case "grant": + guard let ttlStr = args.first, let ttl = Int64(ttlStr) else { + throw EtcdParseError.missingArgument("lease grant requires a TTL (integer seconds)") + } + return .leaseGrant(ttl: ttl) + + case "revoke": + guard let idStr = args.first else { + throw EtcdParseError.missingArgument("lease revoke requires a lease ID") + } + let leaseId = try parseLeaseId(idStr) + return .leaseRevoke(leaseId: leaseId) + + case "timetolive": + guard let idStr = args.first else { + throw EtcdParseError.missingArgument("lease timetolive requires a lease ID") + } + let leaseId = try parseLeaseId(idStr) + var flags = ParsedFlags() + _ = flags.parse(from: Array(args.dropFirst())) + let keys = flags.has("keys") + return .leaseTimetolive(leaseId: leaseId, keys: keys) + + case "list": + return .leaseList + + case "keep-alive": + guard let idStr = args.first else { + throw EtcdParseError.missingArgument("lease keep-alive requires a lease ID") + } + let leaseId = try parseLeaseId(idStr) + return .leaseKeepAlive(leaseId: leaseId) + + default: + throw EtcdParseError.unknownCommand("lease \(subcommand)") + } + } + + // MARK: - Cluster Commands + + private static func parseMember(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("member requires a subcommand (list)") + } + + switch subcommand.lowercased() { + case "list": + return .memberList + default: + throw EtcdParseError.unknownCommand("member \(subcommand)") + } + } + + private static func parseEndpoint(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("endpoint requires a subcommand (status, health)") + } + + switch subcommand.lowercased() { + case "status": + return .endpointStatus + case "health": + return .endpointHealth + default: + throw EtcdParseError.unknownCommand("endpoint \(subcommand)") + } + } + + // MARK: - Maintenance Commands + + private static func parseCompaction(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let revisionStr = positional.first, let revision = Int64(revisionStr) else { + throw EtcdParseError.missingArgument("compaction requires a revision (integer)") + } + + let physical = flags.has("physical") + return .compaction(revision: revision, physical: physical) + } + + // MARK: - Auth Commands + + private static func parseAuth(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("auth requires a subcommand (enable, disable)") + } + + switch subcommand.lowercased() { + case "enable": + return .authEnable + case "disable": + return .authDisable + default: + throw EtcdParseError.unknownCommand("auth \(subcommand)") + } + } + + // MARK: - User Commands + + private static func parseUser(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("user requires a subcommand (add, delete, list, grant-role, revoke-role)") + } + + let args = Array(tokens.dropFirst()) + + switch subcommand.lowercased() { + case "add": + guard let name = args.first else { + throw EtcdParseError.missingArgument("user add requires a username") + } + let password = args.count >= 2 ? args[1] : nil + return .userAdd(name: name, password: password) + + case "delete": + guard let name = args.first else { + throw EtcdParseError.missingArgument("user delete requires a username") + } + return .userDelete(name: name) + + case "list": + return .userList + + case "grant-role": + guard args.count >= 2 else { + throw EtcdParseError.missingArgument("user grant-role requires a username and role") + } + return .userGrantRole(user: args[0], role: args[1]) + + case "revoke-role": + guard args.count >= 2 else { + throw EtcdParseError.missingArgument("user revoke-role requires a username and role") + } + return .userRevokeRole(user: args[0], role: args[1]) + + default: + throw EtcdParseError.unknownCommand("user \(subcommand)") + } + } + + // MARK: - Role Commands + + private static func parseRole(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("role requires a subcommand (add, delete, list)") + } + + let args = Array(tokens.dropFirst()) + + switch subcommand.lowercased() { + case "add": + guard let name = args.first else { + throw EtcdParseError.missingArgument("role add requires a role name") + } + return .roleAdd(name: name) + + case "delete": + guard let name = args.first else { + throw EtcdParseError.missingArgument("role delete requires a role name") + } + return .roleDelete(name: name) + + case "list": + return .roleList + + default: + throw EtcdParseError.unknownCommand("role \(subcommand)") + } + } + + // MARK: - Lease ID Parsing + + static func parseLeaseId(_ string: String) throws -> Int64 { + if string.hasPrefix("0x") || string.hasPrefix("0X") { + let hexStr = String(string.dropFirst(2)) + guard let value = Int64(hexStr, radix: 16) else { + throw EtcdParseError.invalidArgument("Invalid hex lease ID: \(string)") + } + return value + } + + let containsHexChars = string.contains(where: { "abcdefABCDEF".contains($0) }) + if containsHexChars { + guard let value = Int64(string, radix: 16) else { + throw EtcdParseError.invalidArgument("Invalid hex lease ID: \(string)") + } + return value + } + + guard let value = Int64(string) else { + throw EtcdParseError.invalidArgument("Invalid lease ID: \(string)") + } + return value + } + + // MARK: - Tokenizer + + private static func tokenize(_ input: String) -> [String] { + var tokens: [String] = [] + var current = "" + var inQuote = false + var quoteChar: Character = "\"" + var escapeNext = false + + for char in input { + if escapeNext { + current.append(char) + escapeNext = false + continue + } + + if char == "\\" { + escapeNext = true + continue + } + + if inQuote { + if char == quoteChar { + inQuote = false + } else { + current.append(char) + } + continue + } + + if char == "\"" || char == "'" { + inQuote = true + quoteChar = char + continue + } + + if char.isWhitespace { + if !current.isEmpty { + tokens.append(current) + current = "" + } + continue + } + + current.append(char) + } + + if !current.isEmpty { + tokens.append(current) + } + + return tokens + } +} + +// MARK: - Flag Parsing + +private struct ParsedFlags { + private var booleanFlags: Set = [] + private var valueFlags: [String: String] = [:] + + mutating func parse(from tokens: [String]) -> [String] { + var positional: [String] = [] + + for token in tokens { + if token.hasPrefix("--") { + let flagContent = String(token.dropFirst(2)) + if let equalsIndex = flagContent.firstIndex(of: "=") { + let key = String(flagContent[flagContent.startIndex.. Bool { + booleanFlags.contains(flag) + } + + func value(for flag: String) -> String? { + valueFlags[flag] + } +} diff --git a/EtcdDriverPlugin/EtcdHttpClient.swift b/EtcdDriverPlugin/EtcdHttpClient.swift new file mode 100644 index 00000000..52644a95 --- /dev/null +++ b/EtcdDriverPlugin/EtcdHttpClient.swift @@ -0,0 +1,1045 @@ +// +// EtcdHttpClient.swift +// TablePro +// + +import Foundation +import os +import TableProPluginKit + +// MARK: - Error Types + +enum EtcdError: Error, LocalizedError { + case notConnected + case connectionFailed(String) + case serverError(String) + case authFailed(String) + case requestCancelled + + var errorDescription: String? { + switch self { + case .notConnected: + return String(localized: "Not connected to etcd") + case .connectionFailed(let detail): + return String(localized: "Connection failed: \(detail)") + case .serverError(let detail): + return String(localized: "Server error: \(detail)") + case .authFailed(let detail): + return String(localized: "Authentication failed: \(detail)") + case .requestCancelled: + return String(localized: "Request was cancelled") + } + } +} + +// MARK: - Codable Types + +struct EtcdResponseHeader: Decodable { + let clusterId: String? + let memberId: String? + let revision: String? + let raftTerm: String? + + private enum CodingKeys: String, CodingKey { + case clusterId = "cluster_id" + case memberId = "member_id" + case revision + case raftTerm = "raft_term" + } +} + +struct EtcdKeyValue: Decodable { + let key: String + let value: String? + let version: String? + let createRevision: String? + let modRevision: String? + let lease: String? + + private enum CodingKeys: String, CodingKey { + case key + case value + case version + case createRevision = "create_revision" + case modRevision = "mod_revision" + case lease + } +} + +// KV Request/Response + +struct EtcdRangeRequest: Encodable { + let key: String + var rangeEnd: String? + var limit: Int64? + var sortOrder: String? + var sortTarget: String? + var keysOnly: Bool? + var countOnly: Bool? + + private enum CodingKeys: String, CodingKey { + case key + case rangeEnd = "range_end" + case limit + case sortOrder = "sort_order" + case sortTarget = "sort_target" + case keysOnly = "keys_only" + case countOnly = "count_only" + } +} + +struct EtcdRangeResponse: Decodable { + let kvs: [EtcdKeyValue]? + let count: String? + let more: Bool? +} + +struct EtcdPutRequest: Encodable { + let key: String + let value: String + var lease: String? + var prevKv: Bool? + + private enum CodingKeys: String, CodingKey { + case key + case value + case lease + case prevKv = "prev_kv" + } +} + +struct EtcdPutResponse: Decodable { + let header: EtcdResponseHeader? + let prevKv: EtcdKeyValue? + + private enum CodingKeys: String, CodingKey { + case header + case prevKv = "prev_kv" + } +} + +struct EtcdDeleteRequest: Encodable { + let key: String + var rangeEnd: String? + var prevKv: Bool? + + private enum CodingKeys: String, CodingKey { + case key + case rangeEnd = "range_end" + case prevKv = "prev_kv" + } +} + +struct EtcdDeleteResponse: Decodable { + let deleted: String? + let prevKvs: [EtcdKeyValue]? + + private enum CodingKeys: String, CodingKey { + case deleted + case prevKvs = "prev_kvs" + } +} + +// Lease + +struct EtcdLeaseGrantRequest: Encodable { + let TTL: String + var ID: String? +} + +struct EtcdLeaseGrantResponse: Decodable { + let ID: String? + let TTL: String? + let error: String? +} + +struct EtcdLeaseRevokeRequest: Encodable { + let ID: String +} + +struct EtcdLeaseTimeToLiveRequest: Encodable { + let ID: String + let keys: Bool? +} + +struct EtcdLeaseTimeToLiveResponse: Decodable { + let ID: String? + let TTL: String? + let grantedTTL: String? + let keys: [String]? +} + +struct EtcdLeaseListResponse: Decodable { + let leases: [EtcdLeaseStatus]? +} + +struct EtcdLeaseStatus: Decodable { + let ID: String +} + +// Cluster + +struct EtcdMemberListResponse: Decodable { + let members: [EtcdMember]? + let header: EtcdResponseHeader? +} + +struct EtcdMember: Decodable { + let ID: String? + let name: String? + let peerURLs: [String]? + let clientURLs: [String]? + let isLearner: Bool? +} + +struct EtcdStatusResponse: Decodable { + let version: String? + let dbSize: String? + let leader: String? + let raftIndex: String? + let raftTerm: String? + let errors: [String]? +} + +// Watch + +struct EtcdWatchRequest: Encodable { + let createRequest: EtcdWatchCreateRequest + + private enum CodingKeys: String, CodingKey { + case createRequest = "create_request" + } +} + +struct EtcdWatchCreateRequest: Encodable { + let key: String + var rangeEnd: String? + + private enum CodingKeys: String, CodingKey { + case key + case rangeEnd = "range_end" + } +} + +struct EtcdWatchStreamResponse: Decodable { + let result: EtcdWatchResult? +} + +struct EtcdWatchResult: Decodable { + let events: [EtcdWatchEvent]? + let header: EtcdResponseHeader? +} + +struct EtcdWatchEvent: Decodable { + let type: String? + let kv: EtcdKeyValue? + let prevKv: EtcdKeyValue? + + private enum CodingKeys: String, CodingKey { + case type + case kv + case prevKv = "prev_kv" + } +} + +// Auth + +struct EtcdAuthRequest: Encodable { + let name: String + let password: String +} + +struct EtcdAuthResponse: Decodable { + let token: String? +} + +struct EtcdUserAddRequest: Encodable { + let name: String + let password: String +} + +struct EtcdUserDeleteRequest: Encodable { + let name: String +} + +struct EtcdUserListResponse: Decodable { + let users: [String]? +} + +struct EtcdRoleAddRequest: Encodable { + let name: String +} + +struct EtcdRoleDeleteRequest: Encodable { + let name: String +} + +struct EtcdRoleListResponse: Decodable { + let roles: [String]? +} + +struct EtcdUserGrantRoleRequest: Encodable { + let user: String + let role: String +} + +struct EtcdUserRevokeRoleRequest: Encodable { + let user: String + let role: String +} + +// Maintenance + +struct EtcdCompactionRequest: Encodable { + let revision: String + let physical: Bool? +} + +// MARK: - Generic Error Response + +private struct EtcdErrorResponse: Decodable { + let error: String? + let message: String? + let code: Int? +} + +// MARK: - HTTP Client + +final class EtcdHttpClient: @unchecked Sendable { + private let config: DriverConnectionConfig + private let lock = NSLock() + private var session: URLSession? + private var currentTask: URLSessionDataTask? + private var authToken: String? + private var _isAuthenticating = false + + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdHttpClient") + + init(config: DriverConnectionConfig) { + self.config = config + } + + // MARK: - Base URL + + private var tlsEnabled: Bool { + let mode = config.additionalFields["etcdTlsMode"] ?? "Disabled" + return mode != "Disabled" + } + + private var baseUrl: String { + let scheme = tlsEnabled ? "https" : "http" + return "\(scheme)://\(config.host):\(config.port)" + } + + // MARK: - Connection Lifecycle + + func connect() async throws { + let tlsMode = config.additionalFields["etcdTlsMode"] ?? "Disabled" + + let urlConfig = URLSessionConfiguration.default + urlConfig.timeoutIntervalForRequest = 30 + urlConfig.timeoutIntervalForResource = 300 + + let delegate: URLSessionDelegate? + switch tlsMode { + case "Required": + delegate = InsecureTlsDelegate() + case "VerifyCA", "VerifyIdentity": + delegate = EtcdTlsDelegate( + caCertPath: config.additionalFields["etcdCaCertPath"], + clientCertPath: config.additionalFields["etcdClientCertPath"], + clientKeyPath: config.additionalFields["etcdClientKeyPath"], + verifyHostname: tlsMode == "VerifyIdentity" + ) + default: + delegate = nil + } + + lock.lock() + if let delegate { + session = URLSession(configuration: urlConfig, delegate: delegate, delegateQueue: nil) + } else { + session = URLSession(configuration: urlConfig) + } + lock.unlock() + + do { + try await ping() + } catch { + lock.lock() + session?.invalidateAndCancel() + session = nil + lock.unlock() + Self.logger.error("Connection test failed: \(error.localizedDescription)") + throw EtcdError.connectionFailed(error.localizedDescription) + } + + if !config.username.isEmpty { + do { + try await authenticate() + } catch { + lock.lock() + session?.invalidateAndCancel() + session = nil + lock.unlock() + throw error + } + } + + Self.logger.debug("Connected to etcd at \(self.config.host):\(self.config.port)") + } + + func disconnect() { + lock.lock() + currentTask?.cancel() + currentTask = nil + session?.invalidateAndCancel() + session = nil + authToken = nil + _isAuthenticating = false + lock.unlock() + } + + func ping() async throws { + let _: EtcdStatusResponse = try await post(path: "v3/maintenance/status", body: EmptyBody()) + } + + // MARK: - KV Operations + + func rangeRequest(_ req: EtcdRangeRequest) async throws -> EtcdRangeResponse { + try await post(path: "v3/kv/range", body: req) + } + + func putRequest(_ req: EtcdPutRequest) async throws -> EtcdPutResponse { + try await post(path: "v3/kv/put", body: req) + } + + func deleteRequest(_ req: EtcdDeleteRequest) async throws -> EtcdDeleteResponse { + try await post(path: "v3/kv/deleterange", body: req) + } + + // MARK: - Lease Operations + + func leaseGrant(ttl: Int64) async throws -> EtcdLeaseGrantResponse { + let req = EtcdLeaseGrantRequest(TTL: String(ttl)) + return try await post(path: "v3/lease/grant", body: req) + } + + func leaseRevoke(leaseId: Int64) async throws { + let req = EtcdLeaseRevokeRequest(ID: String(leaseId)) + try await postVoid(path: "v3/lease/revoke", body: req) + } + + func leaseTimeToLive(leaseId: Int64, keys: Bool) async throws -> EtcdLeaseTimeToLiveResponse { + let req = EtcdLeaseTimeToLiveRequest(ID: String(leaseId), keys: keys) + return try await post(path: "v3/lease/timetolive", body: req) + } + + func leaseList() async throws -> EtcdLeaseListResponse { + try await post(path: "v3/lease/leases", body: EmptyBody()) + } + + // MARK: - Cluster Operations + + func memberList() async throws -> EtcdMemberListResponse { + try await post(path: "v3/cluster/member/list", body: EmptyBody()) + } + + func endpointStatus() async throws -> EtcdStatusResponse { + try await post(path: "v3/maintenance/status", body: EmptyBody()) + } + + // MARK: - Watch + + func watch(key: String, prefix: Bool, timeout: TimeInterval) async throws -> [EtcdWatchEvent] { + lock.lock() + guard let session else { + lock.unlock() + throw EtcdError.notConnected + } + let token = authToken + lock.unlock() + + let b64Key = Self.base64Encode(key) + var createReq = EtcdWatchCreateRequest(key: b64Key) + if prefix { + createReq.rangeEnd = Self.base64Encode(Self.prefixRangeEnd(for: key)) + } + let watchReq = EtcdWatchRequest(createRequest: createReq) + + guard let url = URL(string: "\(baseUrl)/v3/watch") else { + throw EtcdError.serverError("Invalid URL: \(baseUrl)/v3/watch") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + if let token { + request.setValue(token, forHTTPHeaderField: "Authorization") + } + request.httpBody = try JSONEncoder().encode(watchReq) + + return try await withThrowingTaskGroup(of: [EtcdWatchEvent].self) { group in + let collectedData = DataCollector() + + group.addTask { + let data: Data = try await withCheckedThrowingContinuation { continuation in + let task = session.dataTask(with: request) { data, _, error in + if let error { + // URLError.cancelled is expected when we cancel after timeout + if (error as? URLError)?.code == .cancelled { + continuation.resume(returning: data ?? Data()) + } else { + continuation.resume(throwing: error) + } + return + } + continuation.resume(returning: data ?? Data()) + } + collectedData.task = task + task.resume() + } + return Self.parseWatchEvents(from: data) + } + + group.addTask { + try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + collectedData.task?.cancel() + return [] + } + + var allEvents: [EtcdWatchEvent] = [] + for try await events in group { + allEvents.append(contentsOf: events) + } + group.cancelAll() + return allEvents + } + } + + // MARK: - Auth Management + + func authEnable() async throws { + try await postVoid(path: "v3/auth/enable", body: EmptyBody()) + } + + func authDisable() async throws { + try await postVoid(path: "v3/auth/disable", body: EmptyBody()) + } + + func userAdd(name: String, password: String) async throws { + let req = EtcdUserAddRequest(name: name, password: password) + try await postVoid(path: "v3/auth/user/add", body: req) + } + + func userDelete(name: String) async throws { + let req = EtcdUserDeleteRequest(name: name) + try await postVoid(path: "v3/auth/user/delete", body: req) + } + + func userList() async throws -> [String] { + let resp: EtcdUserListResponse = try await post(path: "v3/auth/user/list", body: EmptyBody()) + return resp.users ?? [] + } + + func roleAdd(name: String) async throws { + let req = EtcdRoleAddRequest(name: name) + try await postVoid(path: "v3/auth/role/add", body: req) + } + + func roleDelete(name: String) async throws { + let req = EtcdRoleDeleteRequest(name: name) + try await postVoid(path: "v3/auth/role/delete", body: req) + } + + func roleList() async throws -> [String] { + let resp: EtcdRoleListResponse = try await post(path: "v3/auth/role/list", body: EmptyBody()) + return resp.roles ?? [] + } + + func userGrantRole(user: String, role: String) async throws { + let req = EtcdUserGrantRoleRequest(user: user, role: role) + try await postVoid(path: "v3/auth/user/grant", body: req) + } + + func userRevokeRole(user: String, role: String) async throws { + let req = EtcdUserRevokeRoleRequest(user: user, role: role) + try await postVoid(path: "v3/auth/user/revoke", body: req) + } + + // MARK: - Maintenance + + func compaction(revision: Int64, physical: Bool) async throws { + let req = EtcdCompactionRequest(revision: String(revision), physical: physical) + try await postVoid(path: "v3/kv/compaction", body: req) + } + + // MARK: - Cancellation + + func cancelCurrentRequest() { + lock.lock() + currentTask?.cancel() + currentTask = nil + lock.unlock() + } + + // MARK: - Internal Transport + + private func post(path: String, body: Req) async throws -> Res { + let data = try await performRequest(path: path, body: body) + do { + let decoder = JSONDecoder() + return try decoder.decode(Res.self, from: data) + } catch { + let bodyStr = String(data: data, encoding: .utf8) ?? "" + Self.logger.error("Failed to decode response for \(path): \(bodyStr)") + throw EtcdError.serverError("Failed to decode response: \(error.localizedDescription)") + } + } + + private func postVoid(path: String, body: Req) async throws { + _ = try await performRequest(path: path, body: body) + } + + private func performRequest(path: String, body: Req) async throws -> Data { + lock.lock() + guard let session else { + lock.unlock() + throw EtcdError.notConnected + } + let token = authToken + lock.unlock() + + guard let url = URL(string: "\(baseUrl)/\(path)") else { + throw EtcdError.serverError("Invalid URL: \(baseUrl)/\(path)") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + if let token { + request.setValue(token, forHTTPHeaderField: "Authorization") + } + request.httpBody = try JSONEncoder().encode(body) + + let (data, response) = try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in + let task = session.dataTask(with: request) { data, response, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let data, let response else { + continuation.resume(throwing: EtcdError.serverError("Empty response from server")) + return + } + continuation.resume(returning: (data, response)) + } + + self.lock.lock() + self.currentTask = task + self.lock.unlock() + + task.resume() + } + } onCancel: { + self.lock.lock() + self.currentTask?.cancel() + self.currentTask = nil + self.lock.unlock() + } + + lock.lock() + currentTask = nil + lock.unlock() + + guard let httpResponse = response as? HTTPURLResponse else { + throw EtcdError.serverError("Invalid response type") + } + + if httpResponse.statusCode == 401 { + // Attempt token refresh if not already authenticating and credentials are available + lock.lock() + let alreadyAuthenticating = _isAuthenticating + lock.unlock() + + if !alreadyAuthenticating, !config.username.isEmpty { + try await authenticate() + return try await performRequest(path: path, body: body) + } + let errorBody = String(data: data, encoding: .utf8) ?? "Unauthorized" + throw EtcdError.authFailed(errorBody) + } + + if httpResponse.statusCode >= 400 { + let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error" + if let errorResp = try? JSONDecoder().decode(EtcdErrorResponse.self, from: data), + let message = errorResp.error ?? errorResp.message { + throw EtcdError.serverError(message) + } + throw EtcdError.serverError(errorBody.trimmingCharacters(in: .whitespacesAndNewlines)) + } + + return data + } + + // MARK: - Authentication + + private func authenticate() async throws { + lock.lock() + guard session != nil else { + lock.unlock() + throw EtcdError.notConnected + } + if _isAuthenticating { + lock.unlock() + return + } + _isAuthenticating = true + lock.unlock() + + defer { + lock.lock() + _isAuthenticating = false + lock.unlock() + } + + let authReq = EtcdAuthRequest(name: config.username, password: config.password) + guard let url = URL(string: "\(baseUrl)/v3/auth/authenticate") else { + throw EtcdError.serverError("Invalid auth URL") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(authReq) + + lock.lock() + guard let session else { + lock.unlock() + throw EtcdError.notConnected + } + lock.unlock() + + let (data, response) = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation<(Data, URLResponse), Error>) in + let task = session.dataTask(with: request) { data, response, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let data, let response else { + continuation.resume(throwing: EtcdError.authFailed("Empty response")) + return + } + continuation.resume(returning: (data, response)) + } + task.resume() + } + + guard let httpResponse = response as? HTTPURLResponse else { + throw EtcdError.authFailed("Invalid response type") + } + + if httpResponse.statusCode >= 400 { + let errorBody = String(data: data, encoding: .utf8) ?? "Authentication failed" + throw EtcdError.authFailed(errorBody) + } + + let authResp = try JSONDecoder().decode(EtcdAuthResponse.self, from: data) + guard let token = authResp.token, !token.isEmpty else { + throw EtcdError.authFailed("No token in response") + } + + lock.lock() + authToken = token + lock.unlock() + + Self.logger.debug("Authenticated with etcd successfully") + } + + // MARK: - Watch Helpers + + private static func parseWatchEvents(from data: Data) -> [EtcdWatchEvent] { + guard !data.isEmpty else { return [] } + guard let text = String(data: data, encoding: .utf8) else { return [] } + + var events: [EtcdWatchEvent] = [] + let decoder = JSONDecoder() + let lines = text.components(separatedBy: "\n") + + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { continue } + guard let lineData = trimmed.data(using: .utf8) else { continue } + + if let streamResp = try? decoder.decode(EtcdWatchStreamResponse.self, from: lineData), + let result = streamResp.result, + let resultEvents = result.events { + events.append(contentsOf: resultEvents) + } else if let result = try? decoder.decode(EtcdWatchResult.self, from: lineData), + let resultEvents = result.events { + events.append(contentsOf: resultEvents) + } + } + return events + } + + // MARK: - Base64 Helpers + + static func base64Encode(_ string: String) -> String { + Data(string.utf8).base64EncodedString() + } + + static func base64Decode(_ string: String) -> String { + guard let data = Data(base64Encoded: string) else { return string } + return String(data: data, encoding: .utf8) ?? "" + } + + static func prefixRangeEnd(for prefix: String) -> String { + // Increment last byte for prefix range queries + var bytes = Array(prefix.utf8) + guard !bytes.isEmpty else { return "\0" } + var i = bytes.count - 1 + while i >= 0 { + if bytes[i] < 0xFF { + bytes[i] += 1 + return String(bytes: Array(bytes[0 ... i]), encoding: .utf8) ?? "\0" + } + i -= 1 + } + return "\0" + } + + // MARK: - Empty Body Helper + + private struct EmptyBody: Encodable {} + + // MARK: - Data Collector for Watch + + private final class DataCollector: @unchecked Sendable { + var task: URLSessionDataTask? + } + + // MARK: - TLS Delegates + + private class InsecureTlsDelegate: NSObject, URLSessionDelegate { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let serverTrust = challenge.protectionSpace.serverTrust { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } else { + completionHandler(.performDefaultHandling, nil) + } + } + } + + private class EtcdTlsDelegate: NSObject, URLSessionDelegate { + private let caCertPath: String? + private let clientCertPath: String? + private let clientKeyPath: String? + private let verifyHostname: Bool + + init( + caCertPath: String?, + clientCertPath: String?, + clientKeyPath: String?, + verifyHostname: Bool + ) { + self.caCertPath = caCertPath + self.clientCertPath = clientCertPath + self.clientKeyPath = clientKeyPath + self.verifyHostname = verifyHostname + } + + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + let authMethod = challenge.protectionSpace.authenticationMethod + + if authMethod == NSURLAuthenticationMethodServerTrust { + handleServerTrust(challenge: challenge, completionHandler: completionHandler) + } else if authMethod == NSURLAuthenticationMethodClientCertificate { + handleClientCertificate(challenge: challenge, completionHandler: completionHandler) + } else { + completionHandler(.performDefaultHandling, nil) + } + } + + private func handleServerTrust( + challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + if let caPath = caCertPath, !caPath.isEmpty { + guard let caData = try? Data(contentsOf: URL(fileURLWithPath: caPath)), + let caCert = SecCertificateCreateWithData(nil, caData as CFData) else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + SecTrustSetAnchorCertificates(serverTrust, [caCert] as CFArray) + SecTrustSetAnchorCertificatesOnly(serverTrust, true) + } + + if !verifyHostname { + // VerifyCA mode: validate the CA chain but skip hostname check + let policy = SecPolicyCreateBasicX509() + SecTrustSetPolicies(serverTrust, policy) + } + + var secResult: SecTrustResultType = .invalid + SecTrustEvaluate(serverTrust, &secResult) + + if secResult == .unspecified || secResult == .proceed { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } + + private func handleClientCertificate( + challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard let certPath = clientCertPath, !certPath.isEmpty, + let keyPath = clientKeyPath, !keyPath.isEmpty else { + completionHandler(.performDefaultHandling, nil) + return + } + + guard let p12Data = buildPkcs12(certPath: certPath, keyPath: keyPath) else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + let options: [String: Any] = [kSecImportExportPassphrase as String: ""] + var items: CFArray? + let status = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &items) + + guard status == errSecSuccess, + let itemArray = items as? [[String: Any]], + let firstItem = itemArray.first, + let identityRef = firstItem[kSecImportItemIdentity as String] else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + // swiftlint:disable:next force_cast + let identity = identityRef as! SecIdentity + let credential = URLCredential( + identity: identity, + certificates: nil, + persistence: .forSession + ) + completionHandler(.useCredential, credential) + } + + private func buildPkcs12(certPath: String, keyPath: String) -> Data? { + // Read PEM cert and key, create identity via SecItemImport + guard let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)), + let keyData = try? Data(contentsOf: URL(fileURLWithPath: keyPath)) else { + return nil + } + + var certItems: CFArray? + var certFormat = SecExternalFormat.formatPEMSequence + var certType = SecExternalItemType.itemTypeCertificate + let certStatus = SecItemImport( + certData as CFData, + nil, + &certFormat, + &certType, + [], + nil, + nil, + &certItems + ) + + guard certStatus == errSecSuccess, + let certs = certItems as? [SecCertificate], + let cert = certs.first else { + return nil + } + + var keyItems: CFArray? + var keyFormat = SecExternalFormat.formatPEMSequence + var keyType = SecExternalItemType.itemTypePrivateKey + let keyStatus = SecItemImport( + keyData as CFData, + nil, + &keyFormat, + &keyType, + [], + nil, + nil, + &keyItems + ) + + guard keyStatus == errSecSuccess, + let keys = keyItems as? [SecKey], + let privateKey = keys.first else { + return nil + } + + // Export to PKCS#12 + var exportItems: CFArray? + guard let identity = createIdentity(certificate: cert, privateKey: privateKey) else { + return nil + } + + var exportParams = SecItemImportExportKeyParameters() + var p12Data: CFData? + let exportStatus = SecItemExport( + identity, + .formatPKCS12, + [], + &exportParams, + &p12Data + ) + + guard exportStatus == errSecSuccess, let data = p12Data else { + _ = exportItems + return nil + } + _ = exportItems + return data as Data + } + + private func createIdentity(certificate: SecCertificate, privateKey: SecKey) -> SecIdentity? { + // Add cert and key to a temporary keychain to get an identity + let addCertQuery: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecValueRef as String: certificate, + kSecReturnRef as String: true + ] + var certRef: CFTypeRef? + SecItemAdd(addCertQuery as CFDictionary, &certRef) + + let addKeyQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecValueRef as String: privateKey, + kSecReturnRef as String: true + ] + var keyRef: CFTypeRef? + SecItemAdd(addKeyQuery as CFDictionary, &keyRef) + + var identity: SecIdentity? + let status = SecIdentityCreateWithCertificate(nil, certificate, &identity) + if status == errSecSuccess { + return identity + } + return nil + } + } +} diff --git a/EtcdDriverPlugin/EtcdPlugin.swift b/EtcdDriverPlugin/EtcdPlugin.swift new file mode 100644 index 00000000..98d9f7cb --- /dev/null +++ b/EtcdDriverPlugin/EtcdPlugin.swift @@ -0,0 +1,118 @@ +// +// EtcdPlugin.swift +// EtcdDriverPlugin +// +// etcd v3 database driver plugin via HTTP/JSON gateway +// + +import Foundation +import os +import TableProPluginKit + +final class EtcdPlugin: NSObject, TableProPlugin, DriverPlugin { + static let pluginName = "etcd Driver" + static let pluginVersion = "1.0.0" + static let pluginDescription = "etcd v3 support via HTTP/JSON gateway" + static let capabilities: [PluginCapability] = [.databaseDriver] + + static let databaseTypeId = "etcd" + static let databaseDisplayName = "etcd" + static let iconName = "cylinder.fill" + static let defaultPort = 2379 + static let additionalDatabaseTypeIds: [String] = [] + + static let navigationModel: NavigationModel = .standard + static let pathFieldRole: PathFieldRole = .database + static let requiresAuthentication = false + static let urlSchemes: [String] = ["etcd", "etcds"] + static let brandColorHex = "#419EDA" + static let queryLanguageName = "etcdctl" + static let editorLanguage: EditorLanguage = .bash + static let supportsForeignKeys = false + static let supportsSchemaEditing = false + static let supportsDatabaseSwitching = false + static let supportsImport = false + static let tableEntityName = "Keys" + static let supportsForeignKeyDisable = false + static let supportsReadOnlyMode = false + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let defaultGroupName = "main" + static let defaultPrimaryKeyColumn: String? = "Key" + static let structureColumnFields: [StructureColumnField] = [.name, .type, .nullable] + static let sqlDialect: SQLDialectDescriptor? = nil + static let columnTypesByCategory: [String: [String]] = ["String": ["string"]] + + static let additionalConnectionFields: [ConnectionField] = [ + ConnectionField( + id: "etcdKeyPrefix", + label: String(localized: "Key Prefix Root"), + placeholder: "/", + section: .advanced + ), + ConnectionField( + id: "etcdTlsMode", + label: String(localized: "TLS Mode"), + fieldType: .dropdown(options: [ + .init(value: "Disabled", label: "Disabled"), + .init(value: "Required", label: String(localized: "Required (skip verify)")), + .init(value: "VerifyCA", label: String(localized: "Verify CA")), + .init(value: "VerifyIdentity", label: String(localized: "Verify Identity")), + ]), + section: .advanced + ), + ConnectionField( + id: "etcdCaCertPath", + label: String(localized: "CA Certificate"), + placeholder: "/path/to/ca.pem", + section: .advanced + ), + ConnectionField( + id: "etcdClientCertPath", + label: String(localized: "Client Certificate"), + placeholder: "/path/to/client.pem", + section: .advanced + ), + ConnectionField( + id: "etcdClientKeyPath", + label: String(localized: "Client Key"), + placeholder: "/path/to/client-key.pem", + section: .advanced + ), + ] + + static var statementCompletions: [CompletionEntry] { + [ + CompletionEntry(label: "get", insertText: "get"), + CompletionEntry(label: "put", insertText: "put"), + CompletionEntry(label: "del", insertText: "del"), + CompletionEntry(label: "watch", insertText: "watch"), + CompletionEntry(label: "lease grant", insertText: "lease grant"), + CompletionEntry(label: "lease revoke", insertText: "lease revoke"), + CompletionEntry(label: "lease timetolive", insertText: "lease timetolive"), + CompletionEntry(label: "lease list", insertText: "lease list"), + CompletionEntry(label: "lease keep-alive", insertText: "lease keep-alive"), + CompletionEntry(label: "member list", insertText: "member list"), + CompletionEntry(label: "endpoint status", insertText: "endpoint status"), + CompletionEntry(label: "endpoint health", insertText: "endpoint health"), + CompletionEntry(label: "compaction", insertText: "compaction"), + CompletionEntry(label: "auth enable", insertText: "auth enable"), + CompletionEntry(label: "auth disable", insertText: "auth disable"), + CompletionEntry(label: "user add", insertText: "user add"), + CompletionEntry(label: "user delete", insertText: "user delete"), + CompletionEntry(label: "user list", insertText: "user list"), + CompletionEntry(label: "role add", insertText: "role add"), + CompletionEntry(label: "role delete", insertText: "role delete"), + CompletionEntry(label: "role list", insertText: "role list"), + CompletionEntry(label: "user grant-role", insertText: "user grant-role"), + CompletionEntry(label: "user revoke-role", insertText: "user revoke-role"), + CompletionEntry(label: "--prefix", insertText: "--prefix"), + CompletionEntry(label: "--limit", insertText: "--limit="), + CompletionEntry(label: "--keys-only", insertText: "--keys-only"), + CompletionEntry(label: "--lease", insertText: "--lease="), + ] + } + + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { + EtcdPluginDriver(config: config) + } +} diff --git a/EtcdDriverPlugin/EtcdPluginDriver.swift b/EtcdDriverPlugin/EtcdPluginDriver.swift new file mode 100644 index 00000000..775ede7f --- /dev/null +++ b/EtcdDriverPlugin/EtcdPluginDriver.swift @@ -0,0 +1,1000 @@ +// +// EtcdPluginDriver.swift +// EtcdDriverPlugin +// +// PluginDatabaseDriver implementation for etcd v3. +// Routes both NoSQL browsing hooks and editor commands through EtcdHttpClient. +// + +import Foundation +import OSLog +import TableProPluginKit + +final class EtcdPluginDriver: PluginDatabaseDriver, @unchecked Sendable { + private let config: DriverConnectionConfig + private var _httpClient: EtcdHttpClient? + private let lock = NSLock() + private var _serverVersion: String? + private var _rootPrefix: String + + private var httpClient: EtcdHttpClient? { + lock.withLock { _httpClient } + } + + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdPluginDriver") + private static let maxKeys = PluginRowLimits.defaultMax + private static let maxOffset = 10_000 + + private static let columns = ["Key", "Value", "Version", "ModRevision", "CreateRevision", "Lease"] + private static let columnTypeNames = ["String", "String", "Int64", "Int64", "Int64", "String"] + + var serverVersion: String? { + lock.withLock { _serverVersion } + } + + var supportsTransactions: Bool { false } + + func quoteIdentifier(_ name: String) -> String { name } + + func defaultExportQuery(table: String) -> String? { + let prefix = resolvedPrefix(for: table) + return "get \(escapeArgument(prefix)) --prefix" + } + + init(config: DriverConnectionConfig) { + self.config = config + self._rootPrefix = config.additionalFields["etcdKeyPrefix"] ?? config.database + } + + // MARK: - Connection Management + + func connect() async throws { + let client = EtcdHttpClient(config: config) + try await client.connect() + + let status = try? await client.endpointStatus() + lock.withLock { + _serverVersion = status?.version + } + + lock.withLock { _httpClient = client } + } + + func disconnect() { + lock.withLock { + _httpClient?.disconnect() + _httpClient = nil + } + } + + func ping() async throws { + guard let client = httpClient else { + throw EtcdError.notConnected + } + try await client.ping() + } + + // MARK: - Query Execution + + func execute(query: String) async throws -> PluginQueryResult { + let startTime = Date() + + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + + // Health monitor sends "SELECT 1" as a ping + if trimmed.lowercased() == "select 1" { + try await client.ping() + return PluginQueryResult( + columns: ["ok"], + columnTypeNames: ["Int32"], + rows: [["1"]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + // Check for tagged browsing queries + if EtcdQueryBuilder.isTaggedQuery(trimmed) { + return try await executeTaggedQuery(trimmed, client: client, startTime: startTime) + } + + let operation = try EtcdCommandParser.parse(trimmed) + return try await dispatch(operation, client: client, startTime: startTime) + } + + func executeParameterized(query: String, parameters: [String?]) async throws -> PluginQueryResult { + try await execute(query: query) + } + + func fetchRowCount(query: String) async throws -> Int { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + + if let parsed = EtcdQueryBuilder.parseRangeQuery(trimmed) { + return try await countKeys(prefix: parsed.prefix, filterType: parsed.filterType, filterValue: parsed.filterValue, client: client) + } + + if let parsed = EtcdQueryBuilder.parseCountQuery(trimmed) { + return try await countKeys(prefix: parsed.prefix, filterType: parsed.filterType, filterValue: parsed.filterValue, client: client) + } + + return 0 + } + + func fetchRows(query: String, offset: Int, limit: Int) async throws -> PluginQueryResult { + let startTime = Date() + + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + + if let parsed = EtcdQueryBuilder.parseRangeQuery(trimmed) { + return try await fetchKeysPage( + prefix: parsed.prefix, + offset: offset, + limit: limit, + sortAscending: parsed.sortAscending, + filterType: parsed.filterType, + filterValue: parsed.filterValue, + client: client, + startTime: startTime + ) + } + + return try await execute(query: query) + } + + // MARK: - Query Cancellation + + func cancelQuery() throws { + httpClient?.cancelCurrentRequest() + } + + func applyQueryTimeout(_ seconds: Int) async throws {} + + // MARK: - Schema Operations + + func fetchTables(schema: String?) async throws -> [PluginTableInfo] { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let prefix = _rootPrefix + let (b64Key, b64RangeEnd) = Self.allKeysRange(for: prefix) + + let response = try await client.rangeRequest(EtcdRangeRequest( + key: b64Key, + rangeEnd: b64RangeEnd, + limit: Int64(Self.maxKeys), + keysOnly: true + )) + + guard let kvs = response.kvs, !kvs.isEmpty else { + return [PluginTableInfo(name: "(root)", type: "PREFIX", rowCount: 0)] + } + + var prefixCounts: [String: Int] = [:] + var bareKeyCount = 0 + + for kv in kvs { + let key = EtcdHttpClient.base64Decode(kv.key) + let relative = stripRootPrefix(key) + + // Skip leading "/" when finding the first segment + let searchStart: String.Index + if relative.hasPrefix("/") && relative.count > 1 { + searchStart = relative.index(after: relative.startIndex) + } else { + searchStart = relative.startIndex + } + + if let slashIndex = relative[searchStart...].firstIndex(of: "/") { + // Include everything up to and including the slash (and leading / if present) + let segment = String(relative[relative.startIndex...slashIndex]) + prefixCounts[segment, default: 0] += 1 + } else { + bareKeyCount += 1 + } + } + + var tables: [PluginTableInfo] = [] + + if bareKeyCount > 0 { + tables.append(PluginTableInfo(name: "(root)", type: "PREFIX", rowCount: bareKeyCount)) + } + + for (prefixName, count) in prefixCounts.sorted(by: { $0.key < $1.key }) { + tables.append(PluginTableInfo(name: prefixName, type: "PREFIX", rowCount: count)) + } + + if tables.isEmpty { + tables.append(PluginTableInfo(name: "(root)", type: "PREFIX", rowCount: 0)) + } + + return tables + } + + func fetchColumns(table: String, schema: String?) async throws -> [PluginColumnInfo] { + [ + PluginColumnInfo(name: "Key", dataType: "String", isNullable: false, isPrimaryKey: true), + PluginColumnInfo(name: "Value", dataType: "String", isNullable: true), + PluginColumnInfo(name: "Version", dataType: "Int64", isNullable: false), + PluginColumnInfo(name: "ModRevision", dataType: "Int64", isNullable: false), + PluginColumnInfo(name: "CreateRevision", dataType: "Int64", isNullable: false), + PluginColumnInfo(name: "Lease", dataType: "String", isNullable: true), + ] + } + + func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] { + let tables = try await fetchTables(schema: schema) + let columns = try await fetchColumns(table: "", schema: schema) + var result: [String: [PluginColumnInfo]] = [:] + for table in tables { + result[table.name] = columns + } + return result + } + + func fetchIndexes(table: String, schema: String?) async throws -> [PluginIndexInfo] { + [] + } + + func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] { + [] + } + + func fetchApproximateRowCount(table: String, schema: String?) async throws -> Int? { + guard let client = httpClient else { + throw EtcdError.notConnected + } + let prefix = resolvedPrefix(for: table) + return try await countKeys(prefix: prefix, filterType: .none, filterValue: "", client: client) + } + + func fetchTableDDL(table: String, schema: String?) async throws -> String { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let prefix = resolvedPrefix(for: table) + let count = try await countKeys(prefix: prefix, filterType: .none, filterValue: "", client: client) + + return """ + // etcd key prefix: \(prefix.isEmpty ? "(all keys)" : prefix) + // Keys: \(count) + // Use 'get \(prefix.isEmpty ? "/" : prefix) --prefix' to browse keys + """ + } + + func fetchViewDefinition(view: String, schema: String?) async throws -> String { + "" + } + + func fetchTableMetadata(table: String, schema: String?) async throws -> PluginTableMetadata { + return PluginTableMetadata( + tableName: table, + engine: "etcd v3" + ) + } + + func fetchDatabases() async throws -> [String] { + ["default"] + } + + func fetchDatabaseMetadata(_ database: String) async throws -> PluginDatabaseMetadata { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let status = try await client.endpointStatus() + let dbSizeBytes = Int64(status.dbSize ?? "0") + return PluginDatabaseMetadata( + name: database, + sizeBytes: dbSizeBytes + ) + } + + // MARK: - NoSQL Query Building Hooks + + func buildBrowseQuery( + table: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + columns: [String], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildBrowseQuery( + prefix: prefix, sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + func buildFilteredQuery( + table: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + columns: [String], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildFilteredQuery( + prefix: prefix, filters: filters, logicMode: logicMode, + sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + func buildQuickSearchQuery( + table: String, + searchText: String, + columns: [String], + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildQuickSearchQuery( + prefix: prefix, searchText: searchText, + sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + func buildCombinedQuery( + table: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + searchText: String, + searchColumns: [String], + sortColumns: [(columnIndex: Int, ascending: Bool)], + columns: [String], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildCombinedQuery( + prefix: prefix, filters: filters, logicMode: logicMode, + searchText: searchText, sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + // MARK: - Statement Generation + + func generateStatements( + table: String, + columns: [String], + changes: [PluginRowChange], + insertedRowData: [Int: [String?]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [(statement: String, parameters: [String?])]? { + let generator = EtcdStatementGenerator( + prefix: resolvedPrefix(for: table), + columns: columns + ) + return generator.generateStatements( + from: changes, + insertedRowData: insertedRowData, + deletedRowIndices: deletedRowIndices, + insertedRowIndices: insertedRowIndices + ) + } + + func allTablesMetadataSQL(schema: String?) -> String? { + let prefix = _rootPrefix + return "get \(escapeArgument(prefix.isEmpty ? "/" : prefix)) --prefix --keys-only" + } + + // MARK: - Command Dispatch + + private func dispatch( + _ operation: EtcdOperation, + client: EtcdHttpClient, + startTime: Date + ) async throws -> PluginQueryResult { + switch operation { + case .get(let key, let prefix, let limit, let keysOnly, let sortOrder, let sortTarget): + return try await dispatchGet( + key: key, prefix: prefix, limit: limit, keysOnly: keysOnly, + sortOrder: sortOrder, sortTarget: sortTarget, client: client, startTime: startTime + ) + + case .put(let key, let value, let leaseId): + return try await dispatchPut(key: key, value: value, leaseId: leaseId, client: client, startTime: startTime) + + case .del(let key, let prefix): + return try await dispatchDel(key: key, prefix: prefix, client: client, startTime: startTime) + + case .watch(let key, let prefix, let timeout): + return try await dispatchWatch(key: key, prefix: prefix, timeout: timeout, client: client, startTime: startTime) + + case .leaseGrant(let ttl): + return try await dispatchLeaseGrant(ttl: ttl, client: client, startTime: startTime) + + case .leaseRevoke(let leaseId): + return try await dispatchLeaseRevoke(leaseId: leaseId, client: client, startTime: startTime) + + case .leaseTimetolive(let leaseId, let keys): + return try await dispatchLeaseTimetolive(leaseId: leaseId, keys: keys, client: client, startTime: startTime) + + case .leaseList: + return try await dispatchLeaseList(client: client, startTime: startTime) + + case .leaseKeepAlive(let leaseId): + return try await dispatchLeaseKeepAlive(leaseId: leaseId, client: client, startTime: startTime) + + case .memberList: + return try await dispatchMemberList(client: client, startTime: startTime) + + case .endpointStatus: + return try await dispatchEndpointStatus(client: client, startTime: startTime) + + case .endpointHealth: + return try await dispatchEndpointHealth(client: client, startTime: startTime) + + case .compaction(let revision, let physical): + try await client.compaction(revision: revision, physical: physical) + return PluginQueryResult( + columns: ["Result"], + columnTypeNames: ["String"], + rows: [["Compaction completed at revision \(revision)"]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + + case .authEnable: + try await client.authEnable() + return singleMessageResult("Authentication enabled", startTime: startTime) + + case .authDisable: + try await client.authDisable() + return singleMessageResult("Authentication disabled", startTime: startTime) + + case .userAdd(let name, let password): + try await client.userAdd(name: name, password: password ?? "") + return singleMessageResult("User '\(name)' added", startTime: startTime) + + case .userDelete(let name): + try await client.userDelete(name: name) + return singleMessageResult("User '\(name)' deleted", startTime: startTime) + + case .userList: + let users = try await client.userList() + let rows = users.map { [$0 as String?] } + return PluginQueryResult( + columns: ["User"], + columnTypeNames: ["String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + + case .roleAdd(let name): + try await client.roleAdd(name: name) + return singleMessageResult("Role '\(name)' added", startTime: startTime) + + case .roleDelete(let name): + try await client.roleDelete(name: name) + return singleMessageResult("Role '\(name)' deleted", startTime: startTime) + + case .roleList: + let roles = try await client.roleList() + let rows = roles.map { [$0 as String?] } + return PluginQueryResult( + columns: ["Role"], + columnTypeNames: ["String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + + case .userGrantRole(let user, let role): + try await client.userGrantRole(user: user, role: role) + return singleMessageResult("Role '\(role)' granted to user '\(user)'", startTime: startTime) + + case .userRevokeRole(let user, let role): + try await client.userRevokeRole(user: user, role: role) + return singleMessageResult("Role '\(role)' revoked from user '\(user)'", startTime: startTime) + + case .unknown(let command, let args): + Self.logger.warning("Unknown etcd command: \(command) \(args.joined(separator: " "))") + throw EtcdParseError.unknownCommand(command) + } + } + + // MARK: - KV Dispatch + + private func dispatchGet( + key: String, prefix: Bool, limit: Int64?, keysOnly: Bool, + sortOrder: EtcdSortOrder?, sortTarget: EtcdSortTarget?, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let b64Key = EtcdHttpClient.base64Encode(key) + var req = EtcdRangeRequest(key: b64Key) + + if prefix { + req.rangeEnd = EtcdHttpClient.base64Encode(EtcdHttpClient.prefixRangeEnd(for: key)) + } + if let limit = limit { + req.limit = limit + } + if keysOnly { + req.keysOnly = true + } + if let order = sortOrder { + req.sortOrder = order.rawValue + } + if let target = sortTarget { + req.sortTarget = target.rawValue + } + + let response = try await client.rangeRequest(req) + + if keysOnly { + let rows: [[String?]] = (response.kvs ?? []).map { kv in + [EtcdHttpClient.base64Decode(kv.key)] + } + return PluginQueryResult( + columns: ["Key"], + columnTypeNames: ["String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + return mapKvsToResult(response.kvs ?? [], startTime: startTime) + } + + private func dispatchPut( + key: String, value: String, leaseId: Int64?, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + var req = EtcdPutRequest( + key: EtcdHttpClient.base64Encode(key), + value: EtcdHttpClient.base64Encode(value), + prevKv: true + ) + if let leaseId = leaseId { + req.lease = String(leaseId) + } + + let response = try await client.putRequest(req) + let revision = response.header?.revision ?? "unknown" + + return PluginQueryResult( + columns: ["Key", "Value", "Revision"], + columnTypeNames: ["String", "String", "Int64"], + rows: [[key, value, revision]], + rowsAffected: 1, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchDel( + key: String, prefix: Bool, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + var req = EtcdDeleteRequest( + key: EtcdHttpClient.base64Encode(key), + prevKv: true + ) + if prefix { + req.rangeEnd = EtcdHttpClient.base64Encode(EtcdHttpClient.prefixRangeEnd(for: key)) + } + + let response = try await client.deleteRequest(req) + let deleted = response.deleted ?? "0" + + return PluginQueryResult( + columns: ["Deleted"], + columnTypeNames: ["Int64"], + rows: [[deleted]], + rowsAffected: Int(deleted) ?? 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + // MARK: - Watch Dispatch + + private func dispatchWatch( + key: String, prefix: Bool, timeout: TimeInterval, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let events = try await client.watch(key: key, prefix: prefix, timeout: timeout) + + let rows: [[String?]] = events.map { event in + let eventType = event.type ?? "UNKNOWN" + let eventKey = event.kv.map { EtcdHttpClient.base64Decode($0.key) } ?? "" + let eventValue = event.kv?.value.map { EtcdHttpClient.base64Decode($0) } ?? "" + let modRevision = event.kv?.modRevision ?? "" + let prevValue = event.prevKv?.value.map { EtcdHttpClient.base64Decode($0) } ?? "" + return [eventType, eventKey, eventValue, modRevision, prevValue] + } + + return PluginQueryResult( + columns: ["Type", "Key", "Value", "ModRevision", "PrevValue"], + columnTypeNames: ["String", "String", "String", "Int64", "String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + // MARK: - Lease Dispatch + + private func dispatchLeaseGrant( + ttl: Int64, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.leaseGrant(ttl: ttl) + let leaseIdStr = response.ID ?? "unknown" + let grantedTtl = response.TTL ?? String(ttl) + + let hexId: String + if let idNum = Int64(leaseIdStr) { + hexId = String(idNum, radix: 16) + } else { + hexId = leaseIdStr + } + + return PluginQueryResult( + columns: ["LeaseID", "LeaseID (hex)", "TTL"], + columnTypeNames: ["String", "String", "Int64"], + rows: [[leaseIdStr, hexId, grantedTtl]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchLeaseRevoke( + leaseId: Int64, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + try await client.leaseRevoke(leaseId: leaseId) + let hexId = String(leaseId, radix: 16) + return singleMessageResult("Lease \(hexId) revoked", startTime: startTime) + } + + private func dispatchLeaseTimetolive( + leaseId: Int64, keys: Bool, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.leaseTimeToLive(leaseId: leaseId, keys: keys) + + let idStr = response.ID ?? String(leaseId) + let hexId: String + if let idNum = Int64(idStr) { + hexId = String(idNum, radix: 16) + } else { + hexId = idStr + } + + let ttl = response.TTL ?? "unknown" + let grantedTtl = response.grantedTTL ?? "unknown" + let attachedKeys = (response.keys ?? []) + .map { EtcdHttpClient.base64Decode($0) } + .joined(separator: ", ") + + return PluginQueryResult( + columns: ["LeaseID (hex)", "TTL", "GrantedTTL", "AttachedKeys"], + columnTypeNames: ["String", "Int64", "Int64", "String"], + rows: [[hexId, ttl, grantedTtl, attachedKeys]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchLeaseList( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.leaseList() + let rows: [[String?]] = (response.leases ?? []).map { lease in + let idStr = lease.ID + let hexId: String + if let idNum = Int64(idStr) { + hexId = String(idNum, radix: 16) + } else { + hexId = idStr + } + return [idStr, hexId] + } + + return PluginQueryResult( + columns: ["LeaseID", "LeaseID (hex)"], + columnTypeNames: ["String", "String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchLeaseKeepAlive( + leaseId: Int64, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + // lease keep-alive requires a streaming gRPC connection not available via HTTP gateway. + // Show the current TTL instead so the user can see the lease status. + let response = try await client.leaseTimeToLive(leaseId: leaseId, keys: false) + let ttl = response.TTL ?? "unknown" + let hexId = String(leaseId, radix: 16) + return singleMessageResult("Lease \(hexId) current TTL: \(ttl)s (keep-alive requires streaming; use etcdctl CLI for persistent keep-alive)", startTime: startTime) + } + + // MARK: - Cluster Dispatch + + private func dispatchMemberList( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.memberList() + let rows: [[String?]] = (response.members ?? []).map { member in + let id = member.ID ?? "unknown" + let hexId: String + if let idNum = UInt64(id) { + hexId = String(idNum, radix: 16) + } else { + hexId = id + } + let name = member.name ?? "" + let peerUrls = (member.peerURLs ?? []).joined(separator: ", ") + let clientUrls = (member.clientURLs ?? []).joined(separator: ", ") + let isLearner = member.isLearner == true ? "true" : "false" + return [hexId, name, peerUrls, clientUrls, isLearner] + } + + return PluginQueryResult( + columns: ["ID", "Name", "PeerURLs", "ClientURLs", "IsLearner"], + columnTypeNames: ["String", "String", "String", "String", "String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchEndpointStatus( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let status = try await client.endpointStatus() + + let version = status.version ?? "unknown" + let dbSize = status.dbSize ?? "unknown" + let leader = status.leader ?? "unknown" + let raftIndex = status.raftIndex ?? "unknown" + let raftTerm = status.raftTerm ?? "unknown" + let errors = (status.errors ?? []).joined(separator: "; ") + + return PluginQueryResult( + columns: ["Version", "DbSize", "Leader", "RaftIndex", "RaftTerm", "Errors"], + columnTypeNames: ["String", "String", "String", "String", "String", "String"], + rows: [[version, dbSize, leader, raftIndex, raftTerm, errors.isEmpty ? nil : errors]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchEndpointHealth( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + try await client.ping() + return singleMessageResult("endpoint is healthy", startTime: startTime) + } + + // MARK: - Tagged Query Execution + + private func executeTaggedQuery( + _ query: String, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + if let parsed = EtcdQueryBuilder.parseRangeQuery(query) { + return try await fetchKeysPage( + prefix: parsed.prefix, + offset: parsed.offset, + limit: parsed.limit, + sortAscending: parsed.sortAscending, + filterType: parsed.filterType, + filterValue: parsed.filterValue, + client: client, + startTime: startTime + ) + } + + if let parsed = EtcdQueryBuilder.parseCountQuery(query) { + let count = try await countKeys(prefix: parsed.prefix, filterType: parsed.filterType, filterValue: parsed.filterValue, client: client) + return PluginQueryResult( + columns: ["Count"], + columnTypeNames: ["Int64"], + rows: [[String(count)]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + throw EtcdError.serverError("Invalid tagged query format") + } + + // MARK: - Key Fetching + + private func fetchKeysPage( + prefix: String, + offset: Int, + limit: Int, + sortAscending: Bool, + filterType: EtcdFilterType, + filterValue: String, + client: EtcdHttpClient, + startTime: Date + ) async throws -> PluginQueryResult { + let (b64Key, b64RangeEnd) = Self.allKeysRange(for: prefix) + + let needsClientFilter = filterType != .none + + // Fetch enough keys to cover offset + limit + client filtering + let fetchLimit = needsClientFilter ? Int64(Self.maxKeys) : Int64(min(offset + limit, Self.maxKeys)) + + var req = EtcdRangeRequest(key: b64Key, rangeEnd: b64RangeEnd, limit: fetchLimit) + req.sortOrder = sortAscending ? "ASCEND" : "DESCEND" + req.sortTarget = "KEY" + + let response = try await client.rangeRequest(req) + var kvs = response.kvs ?? [] + + // Apply client-side filter if needed + if needsClientFilter { + kvs = kvs.filter { kv in + let key = EtcdHttpClient.base64Decode(kv.key) + return matchesFilter(key: key, filterType: filterType, filterValue: filterValue) + } + } + + // Apply pagination + let total = kvs.count + guard offset < total else { + return emptyResult(startTime: startTime) + } + let pageEnd = min(offset + limit, total) + let pageKvs = Array(kvs[offset ..< pageEnd]) + + return mapKvsToResult(pageKvs, startTime: startTime) + } + + private func countKeys( + prefix: String, + filterType: EtcdFilterType, + filterValue: String, + client: EtcdHttpClient + ) async throws -> Int { + let (b64Key, b64RangeEnd) = Self.allKeysRange(for: prefix) + + if filterType == .none { + var req = EtcdRangeRequest(key: b64Key, rangeEnd: b64RangeEnd, limit: Int64(Self.maxKeys)) + req.countOnly = true + let response = try await client.rangeRequest(req) + return Int(response.count ?? "0") ?? 0 + } + + // Need to fetch keys and filter client-side + let req = EtcdRangeRequest(key: b64Key, rangeEnd: b64RangeEnd, limit: Int64(Self.maxKeys), keysOnly: true) + let response = try await client.rangeRequest(req) + let kvs = response.kvs ?? [] + + return kvs.filter { kv in + let key = EtcdHttpClient.base64Decode(kv.key) + return matchesFilter(key: key, filterType: filterType, filterValue: filterValue) + }.count + } + + // MARK: - Helpers + + /// Returns (base64Key, base64RangeEnd) for a prefix range query. + /// Empty prefix uses null byte (\0) as key to mean "all keys". + private static func allKeysRange(for prefix: String) -> (key: String, rangeEnd: String) { + if prefix.isEmpty { + // \0 as key = start from beginning, \0 as range_end = all keys + let b64Key = EtcdHttpClient.base64Encode("\0") + let b64RangeEnd = EtcdHttpClient.base64Encode("\0") + return (b64Key, b64RangeEnd) + } + let b64Key = EtcdHttpClient.base64Encode(prefix) + let b64RangeEnd = EtcdHttpClient.base64Encode(EtcdHttpClient.prefixRangeEnd(for: prefix)) + return (b64Key, b64RangeEnd) + } + + private func resolvedPrefix(for table: String) -> String { + if table == "(root)" { + return _rootPrefix + } + if _rootPrefix.isEmpty { + return table + } + let root = _rootPrefix.hasSuffix("/") ? _rootPrefix : _rootPrefix + "/" + return root + table + } + + private func stripRootPrefix(_ key: String) -> String { + guard !_rootPrefix.isEmpty else { return key } + let root = _rootPrefix.hasSuffix("/") ? _rootPrefix : _rootPrefix + "/" + if key.hasPrefix(root) { + return String(key.dropFirst(root.count)) + } + return key + } + + private func matchesFilter(key: String, filterType: EtcdFilterType, filterValue: String) -> Bool { + switch filterType { + case .none: + return true + case .contains: + return key.localizedCaseInsensitiveContains(filterValue) + case .startsWith: + return key.lowercased().hasPrefix(filterValue.lowercased()) + case .endsWith: + return key.lowercased().hasSuffix(filterValue.lowercased()) + case .equals: + return key == filterValue + } + } + + private func mapKvsToResult(_ kvs: [EtcdKeyValue], startTime: Date) -> PluginQueryResult { + let rows: [[String?]] = kvs.map { kv in + let key = EtcdHttpClient.base64Decode(kv.key) + let value = kv.value.map { EtcdHttpClient.base64Decode($0) } + let version = kv.version ?? "0" + let modRevision = kv.modRevision ?? "0" + let createRevision = kv.createRevision ?? "0" + let lease = kv.lease ?? "0" + let leaseDisplay = lease == "0" ? "" : formatLeaseHex(lease) + return [key, value, version, modRevision, createRevision, leaseDisplay] + } + + return PluginQueryResult( + columns: Self.columns, + columnTypeNames: Self.columnTypeNames, + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func emptyResult(startTime: Date) -> PluginQueryResult { + PluginQueryResult( + columns: Self.columns, + columnTypeNames: Self.columnTypeNames, + rows: [], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func singleMessageResult(_ message: String, startTime: Date) -> PluginQueryResult { + PluginQueryResult( + columns: ["Result"], + columnTypeNames: ["String"], + rows: [[message]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func formatLeaseHex(_ leaseStr: String) -> String { + if let leaseNum = Int64(leaseStr) { + return String(leaseNum, radix: 16) + } + return leaseStr + } + + private func escapeArgument(_ value: String) -> String { + let needsQuoting = value.isEmpty || value.contains(where: { $0.isWhitespace || $0 == "\"" || $0 == "'" }) + if needsQuoting { + let escaped = value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + return "\"\(escaped)\"" + } + return value + } +} diff --git a/EtcdDriverPlugin/EtcdQueryBuilder.swift b/EtcdDriverPlugin/EtcdQueryBuilder.swift new file mode 100644 index 00000000..ce4d9a93 --- /dev/null +++ b/EtcdDriverPlugin/EtcdQueryBuilder.swift @@ -0,0 +1,202 @@ +// +// EtcdQueryBuilder.swift +// EtcdDriverPlugin +// +// Builds internal query strings for etcd key browsing and filtering. +// + +import Foundation +import TableProPluginKit + +enum EtcdFilterType: String { + case none + case contains + case startsWith + case endsWith + case equals +} + +struct EtcdParsedQuery { + let prefix: String + let limit: Int + let offset: Int + let sortAscending: Bool + let filterType: EtcdFilterType + let filterValue: String +} + +struct EtcdParsedCountQuery { + let prefix: String + let filterType: EtcdFilterType + let filterValue: String +} + +struct EtcdQueryBuilder { + static let rangeTag = "ETCD_RANGE:" + static let countTag = "ETCD_COUNT:" + + func buildBrowseQuery( + prefix: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: .none, filterValue: "" + ) + } + + func buildFilteredQuery( + prefix: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + let (filterType, filterValue) = extractKeyFilter(from: filters) + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: filterType, filterValue: filterValue + ) + } + + func buildQuickSearchQuery( + prefix: String, + searchText: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: .contains, filterValue: searchText + ) + } + + func buildCombinedQuery( + prefix: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + searchText: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + if !searchText.isEmpty { + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: .contains, filterValue: searchText + ) + } + let (filterType, filterValue) = extractKeyFilter(from: filters) + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: filterType, filterValue: filterValue + ) + } + + func buildCountQuery(prefix: String) -> String { + Self.encodeCountQuery(prefix: prefix, filterType: .none, filterValue: "") + } + + // MARK: - Encoding + + private static func encodeRangeQuery( + prefix: String, limit: Int, offset: Int, + sortAscending: Bool, filterType: EtcdFilterType, filterValue: String + ) -> String { + let b64Prefix = Data(prefix.utf8).base64EncodedString() + let b64Filter = Data(filterValue.utf8).base64EncodedString() + return "\(rangeTag)\(b64Prefix):\(limit):\(offset):\(sortAscending ? "1" : "0"):\(filterType.rawValue):\(b64Filter)" + } + + private static func encodeCountQuery( + prefix: String, filterType: EtcdFilterType, filterValue: String + ) -> String { + let b64Prefix = Data(prefix.utf8).base64EncodedString() + let b64Filter = Data(filterValue.utf8).base64EncodedString() + return "\(countTag)\(b64Prefix):\(filterType.rawValue):\(b64Filter)" + } + + // MARK: - Decoding + + static func parseRangeQuery(_ query: String) -> EtcdParsedQuery? { + guard query.hasPrefix(rangeTag) else { return nil } + let body = String(query.dropFirst(rangeTag.count)) + let parts = body.components(separatedBy: ":") + guard parts.count >= 6 else { return nil } + + guard let prefixData = Data(base64Encoded: parts[0]), + let prefix = String(data: prefixData, encoding: .utf8), + let limit = Int(parts[1]), + let offset = Int(parts[2]) else { return nil } + + let sortAscending = parts[3] == "1" + let filterType = EtcdFilterType(rawValue: parts[4]) ?? .none + + let filterB64 = parts[5...].joined(separator: ":") + let filterValue: String + if let filterData = Data(base64Encoded: filterB64), + let decoded = String(data: filterData, encoding: .utf8) { + filterValue = decoded + } else { + filterValue = "" + } + + return EtcdParsedQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAscending, filterType: filterType, filterValue: filterValue + ) + } + + static func parseCountQuery(_ query: String) -> EtcdParsedCountQuery? { + guard query.hasPrefix(countTag) else { return nil } + let body = String(query.dropFirst(countTag.count)) + let parts = body.components(separatedBy: ":") + guard parts.count >= 3 else { return nil } + + guard let prefixData = Data(base64Encoded: parts[0]), + let prefix = String(data: prefixData, encoding: .utf8) else { return nil } + + let filterType = EtcdFilterType(rawValue: parts[1]) ?? .none + let filterB64 = parts[2...].joined(separator: ":") + let filterValue: String + if let filterData = Data(base64Encoded: filterB64), + let decoded = String(data: filterData, encoding: .utf8) { + filterValue = decoded + } else { + filterValue = "" + } + + return EtcdParsedCountQuery( + prefix: prefix, filterType: filterType, filterValue: filterValue + ) + } + + static func isTaggedQuery(_ query: String) -> Bool { + query.hasPrefix(rangeTag) || query.hasPrefix(countTag) + } + + // MARK: - Filter Extraction + + private func extractKeyFilter( + from filters: [(column: String, op: String, value: String)] + ) -> (EtcdFilterType, String) { + let keyFilters = filters.filter { $0.column == "Key" } + guard let filter = keyFilters.first else { return (.none, "") } + + switch filter.op { + case "CONTAINS": return (.contains, filter.value) + case "STARTS WITH": return (.startsWith, filter.value) + case "ENDS WITH": return (.endsWith, filter.value) + case "=": return (.equals, filter.value) + default: return (.none, "") + } + } +} diff --git a/EtcdDriverPlugin/EtcdStatementGenerator.swift b/EtcdDriverPlugin/EtcdStatementGenerator.swift new file mode 100644 index 00000000..1a91bd72 --- /dev/null +++ b/EtcdDriverPlugin/EtcdStatementGenerator.swift @@ -0,0 +1,153 @@ +// +// EtcdStatementGenerator.swift +// EtcdDriverPlugin +// +// Generates etcdctl commands from tracked cell changes. +// + +import Foundation +import os +import TableProPluginKit + +struct EtcdStatementGenerator { + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdStatementGenerator") + + let prefix: String + let columns: [String] + + var keyColumnIndex: Int? { columns.firstIndex(of: "Key") } + private var valueColumnIndex: Int? { columns.firstIndex(of: "Value") } + private var leaseColumnIndex: Int? { columns.firstIndex(of: "Lease") } + + func generateStatements( + from changes: [PluginRowChange], + insertedRowData: [Int: [String?]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [(statement: String, parameters: [String?])] { + var statements: [(statement: String, parameters: [String?])] = [] + + for change in changes { + switch change.type { + case .insert: + guard insertedRowIndices.contains(change.rowIndex) else { continue } + statements += generateInsert(for: change, insertedRowData: insertedRowData) + case .update: + statements += generateUpdate(for: change) + case .delete: + guard deletedRowIndices.contains(change.rowIndex) else { continue } + if let key = extractKey(from: change) { + statements.append((statement: "del \(escapeArgument(key))", parameters: [])) + } + } + } + + return statements + } + + private func generateInsert( + for change: PluginRowChange, + insertedRowData: [Int: [String?]] + ) -> [(statement: String, parameters: [String?])] { + var key: String? + var value: String? + var leaseId: String? + + if let values = insertedRowData[change.rowIndex] { + if let ki = keyColumnIndex, ki < values.count { key = values[ki] } + if let vi = valueColumnIndex, vi < values.count { value = values[vi] } + if let li = leaseColumnIndex, li < values.count { leaseId = values[li] } + } else { + for cellChange in change.cellChanges { + switch cellChange.columnName { + case "Key": key = cellChange.newValue + case "Value": value = cellChange.newValue + case "Lease": leaseId = cellChange.newValue + default: break + } + } + } + + guard let k = key, !k.isEmpty else { + Self.logger.warning("Skipping INSERT - no key provided") + return [] + } + + let v = value ?? "" + var cmd = "put \(escapeArgument(k)) \(escapeArgument(v))" + if let lease = leaseId, !lease.isEmpty, lease != "0" { + cmd += " --lease=\(lease)" + } + + return [(statement: cmd, parameters: [])] + } + + private func generateUpdate( + for change: PluginRowChange + ) -> [(statement: String, parameters: [String?])] { + guard !change.cellChanges.isEmpty else { return [] } + guard let originalKey = extractKey(from: change) else { + Self.logger.warning("Skipping UPDATE - no original key") + return [] + } + + var statements: [(statement: String, parameters: [String?])] = [] + + let keyChange = change.cellChanges.first { $0.columnName == "Key" } + let newKey = keyChange?.newValue ?? originalKey + + if newKey != originalKey { + statements.append((statement: "del \(escapeArgument(originalKey))", parameters: [])) + } + + let valueChange = change.cellChanges.first { $0.columnName == "Value" } + let leaseChange = change.cellChanges.first { $0.columnName == "Lease" } + + if valueChange != nil || newKey != originalKey { + let newValue = valueChange?.newValue ?? extractOriginalValue(from: change) ?? "" + var cmd = "put \(escapeArgument(newKey)) \(escapeArgument(newValue))" + if let lease = leaseChange?.newValue, !lease.isEmpty, lease != "0" { + cmd += " --lease=\(lease)" + } + statements.append((statement: cmd, parameters: [])) + } else if let lease = leaseChange?.newValue { + let currentValue = extractOriginalValue(from: change) ?? "" + var cmd = "put \(escapeArgument(newKey)) \(escapeArgument(currentValue))" + if !lease.isEmpty && lease != "0" { + cmd += " --lease=\(lease)" + } + statements.append((statement: cmd, parameters: [])) + } + + return statements + } + + // MARK: - Helpers + + private func extractKey(from change: PluginRowChange) -> String? { + guard let keyIndex = keyColumnIndex, + let originalRow = change.originalRow, + keyIndex < originalRow.count else { return nil } + return originalRow[keyIndex] + } + + private func extractOriginalValue(from change: PluginRowChange) -> String? { + guard let valueIndex = valueColumnIndex, + let originalRow = change.originalRow, + valueIndex < originalRow.count else { return nil } + return originalRow[valueIndex] + } + + private func escapeArgument(_ value: String) -> String { + let needsQuoting = value.isEmpty || value.contains(where: { $0.isWhitespace || $0 == "\"" || $0 == "'" }) + if needsQuoting { + let escaped = value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + return "\"\(escaped)\"" + } + return value + } +} diff --git a/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift b/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift new file mode 100644 index 00000000..2a3dfe90 --- /dev/null +++ b/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift @@ -0,0 +1,508 @@ +// +// EtcdCommandParser.swift +// TablePro +// +// Parses etcdctl-compatible command strings into structured operations. +// Supports: get, put, del, watch, lease, member, endpoint, compaction, auth, user, role commands. +// + +import Foundation +import os +import TableProPluginKit + +enum EtcdOperation { + // KV + case get(key: String, prefix: Bool, limit: Int64?, keysOnly: Bool, sortOrder: EtcdSortOrder?, sortTarget: EtcdSortTarget?) + case put(key: String, value: String, leaseId: Int64?) + case del(key: String, prefix: Bool) + case watch(key: String, prefix: Bool, timeout: TimeInterval) + + // Lease + case leaseGrant(ttl: Int64) + case leaseRevoke(leaseId: Int64) + case leaseTimetolive(leaseId: Int64, keys: Bool) + case leaseList + case leaseKeepAlive(leaseId: Int64) + + // Cluster + case memberList + case endpointStatus + case endpointHealth + + // Maintenance + case compaction(revision: Int64, physical: Bool) + + // Auth + case authEnable + case authDisable + case userAdd(name: String, password: String?) + case userDelete(name: String) + case userList + case roleAdd(name: String) + case roleDelete(name: String) + case roleList + case userGrantRole(user: String, role: String) + case userRevokeRole(user: String, role: String) + + // Generic fallback + case unknown(command: String, args: [String]) +} + +enum EtcdSortOrder: String { + case ascend = "ASCEND" + case descend = "DESCEND" +} + +enum EtcdSortTarget: String { + case key = "KEY" + case version = "VERSION" + case createRevision = "CREATE" + case modRevision = "MOD" + case value = "VALUE" +} + +enum EtcdParseError: Error { + case emptySyntax + case unknownCommand(String) + case missingArgument(String) + case invalidArgument(String) +} + +extension EtcdParseError: PluginDriverError { + var pluginErrorMessage: String { + switch self { + case .emptySyntax: return String(localized: "Empty etcd command") + case .unknownCommand(let cmd): return String(localized: "Unknown command: \(cmd)") + case .missingArgument(let msg): return String(localized: "Missing argument: \(msg)") + case .invalidArgument(let msg): return String(localized: "Invalid argument: \(msg)") + } + } +} + +struct EtcdCommandParser { + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdCommandParser") + + // MARK: - Public API + + static func parse(_ input: String) throws -> EtcdOperation { + let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { throw EtcdParseError.emptySyntax } + + let tokens = tokenize(trimmed) + guard let first = tokens.first else { throw EtcdParseError.emptySyntax } + + let command = first.lowercased() + let remaining = Array(tokens.dropFirst()) + + switch command { + case "get": return try parseGet(remaining) + case "put": return try parsePut(remaining) + case "del", "delete": return try parseDel(remaining) + case "watch": return try parseWatch(remaining) + case "lease": return try parseLease(remaining) + case "member": return try parseMember(remaining) + case "endpoint": return try parseEndpoint(remaining) + case "compaction": return try parseCompaction(remaining) + case "auth": return try parseAuth(remaining) + case "user": return try parseUser(remaining) + case "role": return try parseRole(remaining) + default: return .unknown(command: command, args: remaining) + } + } + + // MARK: - KV Commands + + private static func parseGet(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let key = positional.first else { + throw EtcdParseError.missingArgument("get requires a key") + } + + let prefix = flags.has("prefix") + let keysOnly = flags.has("keys-only") + + var limit: Int64? + if let limitStr = flags.value(for: "limit") { + guard let parsed = Int64(limitStr) else { + throw EtcdParseError.invalidArgument("--limit must be an integer") + } + limit = parsed + } + + var sortOrder: EtcdSortOrder? + if let orderStr = flags.value(for: "order") { + guard let parsed = EtcdSortOrder(rawValue: orderStr.uppercased()) else { + throw EtcdParseError.invalidArgument("--order must be ASCEND or DESCEND") + } + sortOrder = parsed + } + + var sortTarget: EtcdSortTarget? + if let sortByStr = flags.value(for: "sort-by") { + guard let parsed = EtcdSortTarget(rawValue: sortByStr.uppercased()) else { + throw EtcdParseError.invalidArgument("--sort-by must be KEY, VERSION, CREATE, MOD, or VALUE") + } + sortTarget = parsed + } + + return .get( + key: key, + prefix: prefix, + limit: limit, + keysOnly: keysOnly, + sortOrder: sortOrder, + sortTarget: sortTarget + ) + } + + private static func parsePut(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard positional.count >= 2 else { + throw EtcdParseError.missingArgument("put requires key and value") + } + + let key = positional[0] + let value = positional[1] + + var leaseId: Int64? + if let leaseStr = flags.value(for: "lease") { + leaseId = try parseLeaseId(leaseStr) + } + + return .put(key: key, value: value, leaseId: leaseId) + } + + private static func parseDel(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let key = positional.first else { + throw EtcdParseError.missingArgument("del requires a key") + } + + let prefix = flags.has("prefix") + return .del(key: key, prefix: prefix) + } + + private static func parseWatch(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let key = positional.first else { + throw EtcdParseError.missingArgument("watch requires a key") + } + + let prefix = flags.has("prefix") + + var timeout: TimeInterval = 30 + if let timeoutStr = flags.value(for: "timeout") { + guard let parsed = TimeInterval(timeoutStr) else { + throw EtcdParseError.invalidArgument("--timeout must be a number") + } + timeout = parsed + } + + return .watch(key: key, prefix: prefix, timeout: timeout) + } + + // MARK: - Lease Commands + + private static func parseLease(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("lease requires a subcommand (grant, revoke, timetolive, list, keep-alive)") + } + + let args = Array(tokens.dropFirst()) + + switch subcommand.lowercased() { + case "grant": + guard let ttlStr = args.first, let ttl = Int64(ttlStr) else { + throw EtcdParseError.missingArgument("lease grant requires a TTL (integer seconds)") + } + return .leaseGrant(ttl: ttl) + + case "revoke": + guard let idStr = args.first else { + throw EtcdParseError.missingArgument("lease revoke requires a lease ID") + } + let leaseId = try parseLeaseId(idStr) + return .leaseRevoke(leaseId: leaseId) + + case "timetolive": + guard let idStr = args.first else { + throw EtcdParseError.missingArgument("lease timetolive requires a lease ID") + } + let leaseId = try parseLeaseId(idStr) + var flags = ParsedFlags() + _ = flags.parse(from: Array(args.dropFirst())) + let keys = flags.has("keys") + return .leaseTimetolive(leaseId: leaseId, keys: keys) + + case "list": + return .leaseList + + case "keep-alive": + guard let idStr = args.first else { + throw EtcdParseError.missingArgument("lease keep-alive requires a lease ID") + } + let leaseId = try parseLeaseId(idStr) + return .leaseKeepAlive(leaseId: leaseId) + + default: + throw EtcdParseError.unknownCommand("lease \(subcommand)") + } + } + + // MARK: - Cluster Commands + + private static func parseMember(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("member requires a subcommand (list)") + } + + switch subcommand.lowercased() { + case "list": + return .memberList + default: + throw EtcdParseError.unknownCommand("member \(subcommand)") + } + } + + private static func parseEndpoint(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("endpoint requires a subcommand (status, health)") + } + + switch subcommand.lowercased() { + case "status": + return .endpointStatus + case "health": + return .endpointHealth + default: + throw EtcdParseError.unknownCommand("endpoint \(subcommand)") + } + } + + // MARK: - Maintenance Commands + + private static func parseCompaction(_ tokens: [String]) throws -> EtcdOperation { + var flags = ParsedFlags() + let positional = flags.parse(from: tokens) + + guard let revisionStr = positional.first, let revision = Int64(revisionStr) else { + throw EtcdParseError.missingArgument("compaction requires a revision (integer)") + } + + let physical = flags.has("physical") + return .compaction(revision: revision, physical: physical) + } + + // MARK: - Auth Commands + + private static func parseAuth(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("auth requires a subcommand (enable, disable)") + } + + switch subcommand.lowercased() { + case "enable": + return .authEnable + case "disable": + return .authDisable + default: + throw EtcdParseError.unknownCommand("auth \(subcommand)") + } + } + + // MARK: - User Commands + + private static func parseUser(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("user requires a subcommand (add, delete, list, grant-role, revoke-role)") + } + + let args = Array(tokens.dropFirst()) + + switch subcommand.lowercased() { + case "add": + guard let name = args.first else { + throw EtcdParseError.missingArgument("user add requires a username") + } + let password = args.count >= 2 ? args[1] : nil + return .userAdd(name: name, password: password) + + case "delete": + guard let name = args.first else { + throw EtcdParseError.missingArgument("user delete requires a username") + } + return .userDelete(name: name) + + case "list": + return .userList + + case "grant-role": + guard args.count >= 2 else { + throw EtcdParseError.missingArgument("user grant-role requires a username and role") + } + return .userGrantRole(user: args[0], role: args[1]) + + case "revoke-role": + guard args.count >= 2 else { + throw EtcdParseError.missingArgument("user revoke-role requires a username and role") + } + return .userRevokeRole(user: args[0], role: args[1]) + + default: + throw EtcdParseError.unknownCommand("user \(subcommand)") + } + } + + // MARK: - Role Commands + + private static func parseRole(_ tokens: [String]) throws -> EtcdOperation { + guard let subcommand = tokens.first else { + throw EtcdParseError.missingArgument("role requires a subcommand (add, delete, list)") + } + + let args = Array(tokens.dropFirst()) + + switch subcommand.lowercased() { + case "add": + guard let name = args.first else { + throw EtcdParseError.missingArgument("role add requires a role name") + } + return .roleAdd(name: name) + + case "delete": + guard let name = args.first else { + throw EtcdParseError.missingArgument("role delete requires a role name") + } + return .roleDelete(name: name) + + case "list": + return .roleList + + default: + throw EtcdParseError.unknownCommand("role \(subcommand)") + } + } + + // MARK: - Lease ID Parsing + + static func parseLeaseId(_ string: String) throws -> Int64 { + if string.hasPrefix("0x") || string.hasPrefix("0X") { + let hexStr = String(string.dropFirst(2)) + guard let value = Int64(hexStr, radix: 16) else { + throw EtcdParseError.invalidArgument("Invalid hex lease ID: \(string)") + } + return value + } + + let containsHexChars = string.contains(where: { "abcdefABCDEF".contains($0) }) + if containsHexChars { + guard let value = Int64(string, radix: 16) else { + throw EtcdParseError.invalidArgument("Invalid hex lease ID: \(string)") + } + return value + } + + guard let value = Int64(string) else { + throw EtcdParseError.invalidArgument("Invalid lease ID: \(string)") + } + return value + } + + // MARK: - Tokenizer + + private static func tokenize(_ input: String) -> [String] { + var tokens: [String] = [] + var current = "" + var inQuote = false + var quoteChar: Character = "\"" + var escapeNext = false + + for char in input { + if escapeNext { + current.append(char) + escapeNext = false + continue + } + + if char == "\\" { + escapeNext = true + continue + } + + if inQuote { + if char == quoteChar { + inQuote = false + } else { + current.append(char) + } + continue + } + + if char == "\"" || char == "'" { + inQuote = true + quoteChar = char + continue + } + + if char.isWhitespace { + if !current.isEmpty { + tokens.append(current) + current = "" + } + continue + } + + current.append(char) + } + + if !current.isEmpty { + tokens.append(current) + } + + return tokens + } +} + +// MARK: - Flag Parsing + +private struct ParsedFlags { + private var booleanFlags: Set = [] + private var valueFlags: [String: String] = [:] + + mutating func parse(from tokens: [String]) -> [String] { + var positional: [String] = [] + + for token in tokens { + if token.hasPrefix("--") { + let flagContent = String(token.dropFirst(2)) + if let equalsIndex = flagContent.firstIndex(of: "=") { + let key = String(flagContent[flagContent.startIndex.. Bool { + booleanFlags.contains(flag) + } + + func value(for flag: String) -> String? { + valueFlags[flag] + } +} diff --git a/Plugins/EtcdDriverPlugin/EtcdHttpClient.swift b/Plugins/EtcdDriverPlugin/EtcdHttpClient.swift new file mode 100644 index 00000000..52644a95 --- /dev/null +++ b/Plugins/EtcdDriverPlugin/EtcdHttpClient.swift @@ -0,0 +1,1045 @@ +// +// EtcdHttpClient.swift +// TablePro +// + +import Foundation +import os +import TableProPluginKit + +// MARK: - Error Types + +enum EtcdError: Error, LocalizedError { + case notConnected + case connectionFailed(String) + case serverError(String) + case authFailed(String) + case requestCancelled + + var errorDescription: String? { + switch self { + case .notConnected: + return String(localized: "Not connected to etcd") + case .connectionFailed(let detail): + return String(localized: "Connection failed: \(detail)") + case .serverError(let detail): + return String(localized: "Server error: \(detail)") + case .authFailed(let detail): + return String(localized: "Authentication failed: \(detail)") + case .requestCancelled: + return String(localized: "Request was cancelled") + } + } +} + +// MARK: - Codable Types + +struct EtcdResponseHeader: Decodable { + let clusterId: String? + let memberId: String? + let revision: String? + let raftTerm: String? + + private enum CodingKeys: String, CodingKey { + case clusterId = "cluster_id" + case memberId = "member_id" + case revision + case raftTerm = "raft_term" + } +} + +struct EtcdKeyValue: Decodable { + let key: String + let value: String? + let version: String? + let createRevision: String? + let modRevision: String? + let lease: String? + + private enum CodingKeys: String, CodingKey { + case key + case value + case version + case createRevision = "create_revision" + case modRevision = "mod_revision" + case lease + } +} + +// KV Request/Response + +struct EtcdRangeRequest: Encodable { + let key: String + var rangeEnd: String? + var limit: Int64? + var sortOrder: String? + var sortTarget: String? + var keysOnly: Bool? + var countOnly: Bool? + + private enum CodingKeys: String, CodingKey { + case key + case rangeEnd = "range_end" + case limit + case sortOrder = "sort_order" + case sortTarget = "sort_target" + case keysOnly = "keys_only" + case countOnly = "count_only" + } +} + +struct EtcdRangeResponse: Decodable { + let kvs: [EtcdKeyValue]? + let count: String? + let more: Bool? +} + +struct EtcdPutRequest: Encodable { + let key: String + let value: String + var lease: String? + var prevKv: Bool? + + private enum CodingKeys: String, CodingKey { + case key + case value + case lease + case prevKv = "prev_kv" + } +} + +struct EtcdPutResponse: Decodable { + let header: EtcdResponseHeader? + let prevKv: EtcdKeyValue? + + private enum CodingKeys: String, CodingKey { + case header + case prevKv = "prev_kv" + } +} + +struct EtcdDeleteRequest: Encodable { + let key: String + var rangeEnd: String? + var prevKv: Bool? + + private enum CodingKeys: String, CodingKey { + case key + case rangeEnd = "range_end" + case prevKv = "prev_kv" + } +} + +struct EtcdDeleteResponse: Decodable { + let deleted: String? + let prevKvs: [EtcdKeyValue]? + + private enum CodingKeys: String, CodingKey { + case deleted + case prevKvs = "prev_kvs" + } +} + +// Lease + +struct EtcdLeaseGrantRequest: Encodable { + let TTL: String + var ID: String? +} + +struct EtcdLeaseGrantResponse: Decodable { + let ID: String? + let TTL: String? + let error: String? +} + +struct EtcdLeaseRevokeRequest: Encodable { + let ID: String +} + +struct EtcdLeaseTimeToLiveRequest: Encodable { + let ID: String + let keys: Bool? +} + +struct EtcdLeaseTimeToLiveResponse: Decodable { + let ID: String? + let TTL: String? + let grantedTTL: String? + let keys: [String]? +} + +struct EtcdLeaseListResponse: Decodable { + let leases: [EtcdLeaseStatus]? +} + +struct EtcdLeaseStatus: Decodable { + let ID: String +} + +// Cluster + +struct EtcdMemberListResponse: Decodable { + let members: [EtcdMember]? + let header: EtcdResponseHeader? +} + +struct EtcdMember: Decodable { + let ID: String? + let name: String? + let peerURLs: [String]? + let clientURLs: [String]? + let isLearner: Bool? +} + +struct EtcdStatusResponse: Decodable { + let version: String? + let dbSize: String? + let leader: String? + let raftIndex: String? + let raftTerm: String? + let errors: [String]? +} + +// Watch + +struct EtcdWatchRequest: Encodable { + let createRequest: EtcdWatchCreateRequest + + private enum CodingKeys: String, CodingKey { + case createRequest = "create_request" + } +} + +struct EtcdWatchCreateRequest: Encodable { + let key: String + var rangeEnd: String? + + private enum CodingKeys: String, CodingKey { + case key + case rangeEnd = "range_end" + } +} + +struct EtcdWatchStreamResponse: Decodable { + let result: EtcdWatchResult? +} + +struct EtcdWatchResult: Decodable { + let events: [EtcdWatchEvent]? + let header: EtcdResponseHeader? +} + +struct EtcdWatchEvent: Decodable { + let type: String? + let kv: EtcdKeyValue? + let prevKv: EtcdKeyValue? + + private enum CodingKeys: String, CodingKey { + case type + case kv + case prevKv = "prev_kv" + } +} + +// Auth + +struct EtcdAuthRequest: Encodable { + let name: String + let password: String +} + +struct EtcdAuthResponse: Decodable { + let token: String? +} + +struct EtcdUserAddRequest: Encodable { + let name: String + let password: String +} + +struct EtcdUserDeleteRequest: Encodable { + let name: String +} + +struct EtcdUserListResponse: Decodable { + let users: [String]? +} + +struct EtcdRoleAddRequest: Encodable { + let name: String +} + +struct EtcdRoleDeleteRequest: Encodable { + let name: String +} + +struct EtcdRoleListResponse: Decodable { + let roles: [String]? +} + +struct EtcdUserGrantRoleRequest: Encodable { + let user: String + let role: String +} + +struct EtcdUserRevokeRoleRequest: Encodable { + let user: String + let role: String +} + +// Maintenance + +struct EtcdCompactionRequest: Encodable { + let revision: String + let physical: Bool? +} + +// MARK: - Generic Error Response + +private struct EtcdErrorResponse: Decodable { + let error: String? + let message: String? + let code: Int? +} + +// MARK: - HTTP Client + +final class EtcdHttpClient: @unchecked Sendable { + private let config: DriverConnectionConfig + private let lock = NSLock() + private var session: URLSession? + private var currentTask: URLSessionDataTask? + private var authToken: String? + private var _isAuthenticating = false + + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdHttpClient") + + init(config: DriverConnectionConfig) { + self.config = config + } + + // MARK: - Base URL + + private var tlsEnabled: Bool { + let mode = config.additionalFields["etcdTlsMode"] ?? "Disabled" + return mode != "Disabled" + } + + private var baseUrl: String { + let scheme = tlsEnabled ? "https" : "http" + return "\(scheme)://\(config.host):\(config.port)" + } + + // MARK: - Connection Lifecycle + + func connect() async throws { + let tlsMode = config.additionalFields["etcdTlsMode"] ?? "Disabled" + + let urlConfig = URLSessionConfiguration.default + urlConfig.timeoutIntervalForRequest = 30 + urlConfig.timeoutIntervalForResource = 300 + + let delegate: URLSessionDelegate? + switch tlsMode { + case "Required": + delegate = InsecureTlsDelegate() + case "VerifyCA", "VerifyIdentity": + delegate = EtcdTlsDelegate( + caCertPath: config.additionalFields["etcdCaCertPath"], + clientCertPath: config.additionalFields["etcdClientCertPath"], + clientKeyPath: config.additionalFields["etcdClientKeyPath"], + verifyHostname: tlsMode == "VerifyIdentity" + ) + default: + delegate = nil + } + + lock.lock() + if let delegate { + session = URLSession(configuration: urlConfig, delegate: delegate, delegateQueue: nil) + } else { + session = URLSession(configuration: urlConfig) + } + lock.unlock() + + do { + try await ping() + } catch { + lock.lock() + session?.invalidateAndCancel() + session = nil + lock.unlock() + Self.logger.error("Connection test failed: \(error.localizedDescription)") + throw EtcdError.connectionFailed(error.localizedDescription) + } + + if !config.username.isEmpty { + do { + try await authenticate() + } catch { + lock.lock() + session?.invalidateAndCancel() + session = nil + lock.unlock() + throw error + } + } + + Self.logger.debug("Connected to etcd at \(self.config.host):\(self.config.port)") + } + + func disconnect() { + lock.lock() + currentTask?.cancel() + currentTask = nil + session?.invalidateAndCancel() + session = nil + authToken = nil + _isAuthenticating = false + lock.unlock() + } + + func ping() async throws { + let _: EtcdStatusResponse = try await post(path: "v3/maintenance/status", body: EmptyBody()) + } + + // MARK: - KV Operations + + func rangeRequest(_ req: EtcdRangeRequest) async throws -> EtcdRangeResponse { + try await post(path: "v3/kv/range", body: req) + } + + func putRequest(_ req: EtcdPutRequest) async throws -> EtcdPutResponse { + try await post(path: "v3/kv/put", body: req) + } + + func deleteRequest(_ req: EtcdDeleteRequest) async throws -> EtcdDeleteResponse { + try await post(path: "v3/kv/deleterange", body: req) + } + + // MARK: - Lease Operations + + func leaseGrant(ttl: Int64) async throws -> EtcdLeaseGrantResponse { + let req = EtcdLeaseGrantRequest(TTL: String(ttl)) + return try await post(path: "v3/lease/grant", body: req) + } + + func leaseRevoke(leaseId: Int64) async throws { + let req = EtcdLeaseRevokeRequest(ID: String(leaseId)) + try await postVoid(path: "v3/lease/revoke", body: req) + } + + func leaseTimeToLive(leaseId: Int64, keys: Bool) async throws -> EtcdLeaseTimeToLiveResponse { + let req = EtcdLeaseTimeToLiveRequest(ID: String(leaseId), keys: keys) + return try await post(path: "v3/lease/timetolive", body: req) + } + + func leaseList() async throws -> EtcdLeaseListResponse { + try await post(path: "v3/lease/leases", body: EmptyBody()) + } + + // MARK: - Cluster Operations + + func memberList() async throws -> EtcdMemberListResponse { + try await post(path: "v3/cluster/member/list", body: EmptyBody()) + } + + func endpointStatus() async throws -> EtcdStatusResponse { + try await post(path: "v3/maintenance/status", body: EmptyBody()) + } + + // MARK: - Watch + + func watch(key: String, prefix: Bool, timeout: TimeInterval) async throws -> [EtcdWatchEvent] { + lock.lock() + guard let session else { + lock.unlock() + throw EtcdError.notConnected + } + let token = authToken + lock.unlock() + + let b64Key = Self.base64Encode(key) + var createReq = EtcdWatchCreateRequest(key: b64Key) + if prefix { + createReq.rangeEnd = Self.base64Encode(Self.prefixRangeEnd(for: key)) + } + let watchReq = EtcdWatchRequest(createRequest: createReq) + + guard let url = URL(string: "\(baseUrl)/v3/watch") else { + throw EtcdError.serverError("Invalid URL: \(baseUrl)/v3/watch") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + if let token { + request.setValue(token, forHTTPHeaderField: "Authorization") + } + request.httpBody = try JSONEncoder().encode(watchReq) + + return try await withThrowingTaskGroup(of: [EtcdWatchEvent].self) { group in + let collectedData = DataCollector() + + group.addTask { + let data: Data = try await withCheckedThrowingContinuation { continuation in + let task = session.dataTask(with: request) { data, _, error in + if let error { + // URLError.cancelled is expected when we cancel after timeout + if (error as? URLError)?.code == .cancelled { + continuation.resume(returning: data ?? Data()) + } else { + continuation.resume(throwing: error) + } + return + } + continuation.resume(returning: data ?? Data()) + } + collectedData.task = task + task.resume() + } + return Self.parseWatchEvents(from: data) + } + + group.addTask { + try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + collectedData.task?.cancel() + return [] + } + + var allEvents: [EtcdWatchEvent] = [] + for try await events in group { + allEvents.append(contentsOf: events) + } + group.cancelAll() + return allEvents + } + } + + // MARK: - Auth Management + + func authEnable() async throws { + try await postVoid(path: "v3/auth/enable", body: EmptyBody()) + } + + func authDisable() async throws { + try await postVoid(path: "v3/auth/disable", body: EmptyBody()) + } + + func userAdd(name: String, password: String) async throws { + let req = EtcdUserAddRequest(name: name, password: password) + try await postVoid(path: "v3/auth/user/add", body: req) + } + + func userDelete(name: String) async throws { + let req = EtcdUserDeleteRequest(name: name) + try await postVoid(path: "v3/auth/user/delete", body: req) + } + + func userList() async throws -> [String] { + let resp: EtcdUserListResponse = try await post(path: "v3/auth/user/list", body: EmptyBody()) + return resp.users ?? [] + } + + func roleAdd(name: String) async throws { + let req = EtcdRoleAddRequest(name: name) + try await postVoid(path: "v3/auth/role/add", body: req) + } + + func roleDelete(name: String) async throws { + let req = EtcdRoleDeleteRequest(name: name) + try await postVoid(path: "v3/auth/role/delete", body: req) + } + + func roleList() async throws -> [String] { + let resp: EtcdRoleListResponse = try await post(path: "v3/auth/role/list", body: EmptyBody()) + return resp.roles ?? [] + } + + func userGrantRole(user: String, role: String) async throws { + let req = EtcdUserGrantRoleRequest(user: user, role: role) + try await postVoid(path: "v3/auth/user/grant", body: req) + } + + func userRevokeRole(user: String, role: String) async throws { + let req = EtcdUserRevokeRoleRequest(user: user, role: role) + try await postVoid(path: "v3/auth/user/revoke", body: req) + } + + // MARK: - Maintenance + + func compaction(revision: Int64, physical: Bool) async throws { + let req = EtcdCompactionRequest(revision: String(revision), physical: physical) + try await postVoid(path: "v3/kv/compaction", body: req) + } + + // MARK: - Cancellation + + func cancelCurrentRequest() { + lock.lock() + currentTask?.cancel() + currentTask = nil + lock.unlock() + } + + // MARK: - Internal Transport + + private func post(path: String, body: Req) async throws -> Res { + let data = try await performRequest(path: path, body: body) + do { + let decoder = JSONDecoder() + return try decoder.decode(Res.self, from: data) + } catch { + let bodyStr = String(data: data, encoding: .utf8) ?? "" + Self.logger.error("Failed to decode response for \(path): \(bodyStr)") + throw EtcdError.serverError("Failed to decode response: \(error.localizedDescription)") + } + } + + private func postVoid(path: String, body: Req) async throws { + _ = try await performRequest(path: path, body: body) + } + + private func performRequest(path: String, body: Req) async throws -> Data { + lock.lock() + guard let session else { + lock.unlock() + throw EtcdError.notConnected + } + let token = authToken + lock.unlock() + + guard let url = URL(string: "\(baseUrl)/\(path)") else { + throw EtcdError.serverError("Invalid URL: \(baseUrl)/\(path)") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + if let token { + request.setValue(token, forHTTPHeaderField: "Authorization") + } + request.httpBody = try JSONEncoder().encode(body) + + let (data, response) = try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in + let task = session.dataTask(with: request) { data, response, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let data, let response else { + continuation.resume(throwing: EtcdError.serverError("Empty response from server")) + return + } + continuation.resume(returning: (data, response)) + } + + self.lock.lock() + self.currentTask = task + self.lock.unlock() + + task.resume() + } + } onCancel: { + self.lock.lock() + self.currentTask?.cancel() + self.currentTask = nil + self.lock.unlock() + } + + lock.lock() + currentTask = nil + lock.unlock() + + guard let httpResponse = response as? HTTPURLResponse else { + throw EtcdError.serverError("Invalid response type") + } + + if httpResponse.statusCode == 401 { + // Attempt token refresh if not already authenticating and credentials are available + lock.lock() + let alreadyAuthenticating = _isAuthenticating + lock.unlock() + + if !alreadyAuthenticating, !config.username.isEmpty { + try await authenticate() + return try await performRequest(path: path, body: body) + } + let errorBody = String(data: data, encoding: .utf8) ?? "Unauthorized" + throw EtcdError.authFailed(errorBody) + } + + if httpResponse.statusCode >= 400 { + let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error" + if let errorResp = try? JSONDecoder().decode(EtcdErrorResponse.self, from: data), + let message = errorResp.error ?? errorResp.message { + throw EtcdError.serverError(message) + } + throw EtcdError.serverError(errorBody.trimmingCharacters(in: .whitespacesAndNewlines)) + } + + return data + } + + // MARK: - Authentication + + private func authenticate() async throws { + lock.lock() + guard session != nil else { + lock.unlock() + throw EtcdError.notConnected + } + if _isAuthenticating { + lock.unlock() + return + } + _isAuthenticating = true + lock.unlock() + + defer { + lock.lock() + _isAuthenticating = false + lock.unlock() + } + + let authReq = EtcdAuthRequest(name: config.username, password: config.password) + guard let url = URL(string: "\(baseUrl)/v3/auth/authenticate") else { + throw EtcdError.serverError("Invalid auth URL") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(authReq) + + lock.lock() + guard let session else { + lock.unlock() + throw EtcdError.notConnected + } + lock.unlock() + + let (data, response) = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation<(Data, URLResponse), Error>) in + let task = session.dataTask(with: request) { data, response, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let data, let response else { + continuation.resume(throwing: EtcdError.authFailed("Empty response")) + return + } + continuation.resume(returning: (data, response)) + } + task.resume() + } + + guard let httpResponse = response as? HTTPURLResponse else { + throw EtcdError.authFailed("Invalid response type") + } + + if httpResponse.statusCode >= 400 { + let errorBody = String(data: data, encoding: .utf8) ?? "Authentication failed" + throw EtcdError.authFailed(errorBody) + } + + let authResp = try JSONDecoder().decode(EtcdAuthResponse.self, from: data) + guard let token = authResp.token, !token.isEmpty else { + throw EtcdError.authFailed("No token in response") + } + + lock.lock() + authToken = token + lock.unlock() + + Self.logger.debug("Authenticated with etcd successfully") + } + + // MARK: - Watch Helpers + + private static func parseWatchEvents(from data: Data) -> [EtcdWatchEvent] { + guard !data.isEmpty else { return [] } + guard let text = String(data: data, encoding: .utf8) else { return [] } + + var events: [EtcdWatchEvent] = [] + let decoder = JSONDecoder() + let lines = text.components(separatedBy: "\n") + + for line in lines { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { continue } + guard let lineData = trimmed.data(using: .utf8) else { continue } + + if let streamResp = try? decoder.decode(EtcdWatchStreamResponse.self, from: lineData), + let result = streamResp.result, + let resultEvents = result.events { + events.append(contentsOf: resultEvents) + } else if let result = try? decoder.decode(EtcdWatchResult.self, from: lineData), + let resultEvents = result.events { + events.append(contentsOf: resultEvents) + } + } + return events + } + + // MARK: - Base64 Helpers + + static func base64Encode(_ string: String) -> String { + Data(string.utf8).base64EncodedString() + } + + static func base64Decode(_ string: String) -> String { + guard let data = Data(base64Encoded: string) else { return string } + return String(data: data, encoding: .utf8) ?? "" + } + + static func prefixRangeEnd(for prefix: String) -> String { + // Increment last byte for prefix range queries + var bytes = Array(prefix.utf8) + guard !bytes.isEmpty else { return "\0" } + var i = bytes.count - 1 + while i >= 0 { + if bytes[i] < 0xFF { + bytes[i] += 1 + return String(bytes: Array(bytes[0 ... i]), encoding: .utf8) ?? "\0" + } + i -= 1 + } + return "\0" + } + + // MARK: - Empty Body Helper + + private struct EmptyBody: Encodable {} + + // MARK: - Data Collector for Watch + + private final class DataCollector: @unchecked Sendable { + var task: URLSessionDataTask? + } + + // MARK: - TLS Delegates + + private class InsecureTlsDelegate: NSObject, URLSessionDelegate { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let serverTrust = challenge.protectionSpace.serverTrust { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } else { + completionHandler(.performDefaultHandling, nil) + } + } + } + + private class EtcdTlsDelegate: NSObject, URLSessionDelegate { + private let caCertPath: String? + private let clientCertPath: String? + private let clientKeyPath: String? + private let verifyHostname: Bool + + init( + caCertPath: String?, + clientCertPath: String?, + clientKeyPath: String?, + verifyHostname: Bool + ) { + self.caCertPath = caCertPath + self.clientCertPath = clientCertPath + self.clientKeyPath = clientKeyPath + self.verifyHostname = verifyHostname + } + + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + let authMethod = challenge.protectionSpace.authenticationMethod + + if authMethod == NSURLAuthenticationMethodServerTrust { + handleServerTrust(challenge: challenge, completionHandler: completionHandler) + } else if authMethod == NSURLAuthenticationMethodClientCertificate { + handleClientCertificate(challenge: challenge, completionHandler: completionHandler) + } else { + completionHandler(.performDefaultHandling, nil) + } + } + + private func handleServerTrust( + challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + if let caPath = caCertPath, !caPath.isEmpty { + guard let caData = try? Data(contentsOf: URL(fileURLWithPath: caPath)), + let caCert = SecCertificateCreateWithData(nil, caData as CFData) else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + SecTrustSetAnchorCertificates(serverTrust, [caCert] as CFArray) + SecTrustSetAnchorCertificatesOnly(serverTrust, true) + } + + if !verifyHostname { + // VerifyCA mode: validate the CA chain but skip hostname check + let policy = SecPolicyCreateBasicX509() + SecTrustSetPolicies(serverTrust, policy) + } + + var secResult: SecTrustResultType = .invalid + SecTrustEvaluate(serverTrust, &secResult) + + if secResult == .unspecified || secResult == .proceed { + completionHandler(.useCredential, URLCredential(trust: serverTrust)) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } + + private func handleClientCertificate( + challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard let certPath = clientCertPath, !certPath.isEmpty, + let keyPath = clientKeyPath, !keyPath.isEmpty else { + completionHandler(.performDefaultHandling, nil) + return + } + + guard let p12Data = buildPkcs12(certPath: certPath, keyPath: keyPath) else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + let options: [String: Any] = [kSecImportExportPassphrase as String: ""] + var items: CFArray? + let status = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &items) + + guard status == errSecSuccess, + let itemArray = items as? [[String: Any]], + let firstItem = itemArray.first, + let identityRef = firstItem[kSecImportItemIdentity as String] else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + // swiftlint:disable:next force_cast + let identity = identityRef as! SecIdentity + let credential = URLCredential( + identity: identity, + certificates: nil, + persistence: .forSession + ) + completionHandler(.useCredential, credential) + } + + private func buildPkcs12(certPath: String, keyPath: String) -> Data? { + // Read PEM cert and key, create identity via SecItemImport + guard let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)), + let keyData = try? Data(contentsOf: URL(fileURLWithPath: keyPath)) else { + return nil + } + + var certItems: CFArray? + var certFormat = SecExternalFormat.formatPEMSequence + var certType = SecExternalItemType.itemTypeCertificate + let certStatus = SecItemImport( + certData as CFData, + nil, + &certFormat, + &certType, + [], + nil, + nil, + &certItems + ) + + guard certStatus == errSecSuccess, + let certs = certItems as? [SecCertificate], + let cert = certs.first else { + return nil + } + + var keyItems: CFArray? + var keyFormat = SecExternalFormat.formatPEMSequence + var keyType = SecExternalItemType.itemTypePrivateKey + let keyStatus = SecItemImport( + keyData as CFData, + nil, + &keyFormat, + &keyType, + [], + nil, + nil, + &keyItems + ) + + guard keyStatus == errSecSuccess, + let keys = keyItems as? [SecKey], + let privateKey = keys.first else { + return nil + } + + // Export to PKCS#12 + var exportItems: CFArray? + guard let identity = createIdentity(certificate: cert, privateKey: privateKey) else { + return nil + } + + var exportParams = SecItemImportExportKeyParameters() + var p12Data: CFData? + let exportStatus = SecItemExport( + identity, + .formatPKCS12, + [], + &exportParams, + &p12Data + ) + + guard exportStatus == errSecSuccess, let data = p12Data else { + _ = exportItems + return nil + } + _ = exportItems + return data as Data + } + + private func createIdentity(certificate: SecCertificate, privateKey: SecKey) -> SecIdentity? { + // Add cert and key to a temporary keychain to get an identity + let addCertQuery: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecValueRef as String: certificate, + kSecReturnRef as String: true + ] + var certRef: CFTypeRef? + SecItemAdd(addCertQuery as CFDictionary, &certRef) + + let addKeyQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecValueRef as String: privateKey, + kSecReturnRef as String: true + ] + var keyRef: CFTypeRef? + SecItemAdd(addKeyQuery as CFDictionary, &keyRef) + + var identity: SecIdentity? + let status = SecIdentityCreateWithCertificate(nil, certificate, &identity) + if status == errSecSuccess { + return identity + } + return nil + } + } +} diff --git a/Plugins/EtcdDriverPlugin/EtcdPlugin.swift b/Plugins/EtcdDriverPlugin/EtcdPlugin.swift new file mode 100644 index 00000000..98d9f7cb --- /dev/null +++ b/Plugins/EtcdDriverPlugin/EtcdPlugin.swift @@ -0,0 +1,118 @@ +// +// EtcdPlugin.swift +// EtcdDriverPlugin +// +// etcd v3 database driver plugin via HTTP/JSON gateway +// + +import Foundation +import os +import TableProPluginKit + +final class EtcdPlugin: NSObject, TableProPlugin, DriverPlugin { + static let pluginName = "etcd Driver" + static let pluginVersion = "1.0.0" + static let pluginDescription = "etcd v3 support via HTTP/JSON gateway" + static let capabilities: [PluginCapability] = [.databaseDriver] + + static let databaseTypeId = "etcd" + static let databaseDisplayName = "etcd" + static let iconName = "cylinder.fill" + static let defaultPort = 2379 + static let additionalDatabaseTypeIds: [String] = [] + + static let navigationModel: NavigationModel = .standard + static let pathFieldRole: PathFieldRole = .database + static let requiresAuthentication = false + static let urlSchemes: [String] = ["etcd", "etcds"] + static let brandColorHex = "#419EDA" + static let queryLanguageName = "etcdctl" + static let editorLanguage: EditorLanguage = .bash + static let supportsForeignKeys = false + static let supportsSchemaEditing = false + static let supportsDatabaseSwitching = false + static let supportsImport = false + static let tableEntityName = "Keys" + static let supportsForeignKeyDisable = false + static let supportsReadOnlyMode = false + static let databaseGroupingStrategy: GroupingStrategy = .flat + static let defaultGroupName = "main" + static let defaultPrimaryKeyColumn: String? = "Key" + static let structureColumnFields: [StructureColumnField] = [.name, .type, .nullable] + static let sqlDialect: SQLDialectDescriptor? = nil + static let columnTypesByCategory: [String: [String]] = ["String": ["string"]] + + static let additionalConnectionFields: [ConnectionField] = [ + ConnectionField( + id: "etcdKeyPrefix", + label: String(localized: "Key Prefix Root"), + placeholder: "/", + section: .advanced + ), + ConnectionField( + id: "etcdTlsMode", + label: String(localized: "TLS Mode"), + fieldType: .dropdown(options: [ + .init(value: "Disabled", label: "Disabled"), + .init(value: "Required", label: String(localized: "Required (skip verify)")), + .init(value: "VerifyCA", label: String(localized: "Verify CA")), + .init(value: "VerifyIdentity", label: String(localized: "Verify Identity")), + ]), + section: .advanced + ), + ConnectionField( + id: "etcdCaCertPath", + label: String(localized: "CA Certificate"), + placeholder: "/path/to/ca.pem", + section: .advanced + ), + ConnectionField( + id: "etcdClientCertPath", + label: String(localized: "Client Certificate"), + placeholder: "/path/to/client.pem", + section: .advanced + ), + ConnectionField( + id: "etcdClientKeyPath", + label: String(localized: "Client Key"), + placeholder: "/path/to/client-key.pem", + section: .advanced + ), + ] + + static var statementCompletions: [CompletionEntry] { + [ + CompletionEntry(label: "get", insertText: "get"), + CompletionEntry(label: "put", insertText: "put"), + CompletionEntry(label: "del", insertText: "del"), + CompletionEntry(label: "watch", insertText: "watch"), + CompletionEntry(label: "lease grant", insertText: "lease grant"), + CompletionEntry(label: "lease revoke", insertText: "lease revoke"), + CompletionEntry(label: "lease timetolive", insertText: "lease timetolive"), + CompletionEntry(label: "lease list", insertText: "lease list"), + CompletionEntry(label: "lease keep-alive", insertText: "lease keep-alive"), + CompletionEntry(label: "member list", insertText: "member list"), + CompletionEntry(label: "endpoint status", insertText: "endpoint status"), + CompletionEntry(label: "endpoint health", insertText: "endpoint health"), + CompletionEntry(label: "compaction", insertText: "compaction"), + CompletionEntry(label: "auth enable", insertText: "auth enable"), + CompletionEntry(label: "auth disable", insertText: "auth disable"), + CompletionEntry(label: "user add", insertText: "user add"), + CompletionEntry(label: "user delete", insertText: "user delete"), + CompletionEntry(label: "user list", insertText: "user list"), + CompletionEntry(label: "role add", insertText: "role add"), + CompletionEntry(label: "role delete", insertText: "role delete"), + CompletionEntry(label: "role list", insertText: "role list"), + CompletionEntry(label: "user grant-role", insertText: "user grant-role"), + CompletionEntry(label: "user revoke-role", insertText: "user revoke-role"), + CompletionEntry(label: "--prefix", insertText: "--prefix"), + CompletionEntry(label: "--limit", insertText: "--limit="), + CompletionEntry(label: "--keys-only", insertText: "--keys-only"), + CompletionEntry(label: "--lease", insertText: "--lease="), + ] + } + + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { + EtcdPluginDriver(config: config) + } +} diff --git a/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift b/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift new file mode 100644 index 00000000..775ede7f --- /dev/null +++ b/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift @@ -0,0 +1,1000 @@ +// +// EtcdPluginDriver.swift +// EtcdDriverPlugin +// +// PluginDatabaseDriver implementation for etcd v3. +// Routes both NoSQL browsing hooks and editor commands through EtcdHttpClient. +// + +import Foundation +import OSLog +import TableProPluginKit + +final class EtcdPluginDriver: PluginDatabaseDriver, @unchecked Sendable { + private let config: DriverConnectionConfig + private var _httpClient: EtcdHttpClient? + private let lock = NSLock() + private var _serverVersion: String? + private var _rootPrefix: String + + private var httpClient: EtcdHttpClient? { + lock.withLock { _httpClient } + } + + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdPluginDriver") + private static let maxKeys = PluginRowLimits.defaultMax + private static let maxOffset = 10_000 + + private static let columns = ["Key", "Value", "Version", "ModRevision", "CreateRevision", "Lease"] + private static let columnTypeNames = ["String", "String", "Int64", "Int64", "Int64", "String"] + + var serverVersion: String? { + lock.withLock { _serverVersion } + } + + var supportsTransactions: Bool { false } + + func quoteIdentifier(_ name: String) -> String { name } + + func defaultExportQuery(table: String) -> String? { + let prefix = resolvedPrefix(for: table) + return "get \(escapeArgument(prefix)) --prefix" + } + + init(config: DriverConnectionConfig) { + self.config = config + self._rootPrefix = config.additionalFields["etcdKeyPrefix"] ?? config.database + } + + // MARK: - Connection Management + + func connect() async throws { + let client = EtcdHttpClient(config: config) + try await client.connect() + + let status = try? await client.endpointStatus() + lock.withLock { + _serverVersion = status?.version + } + + lock.withLock { _httpClient = client } + } + + func disconnect() { + lock.withLock { + _httpClient?.disconnect() + _httpClient = nil + } + } + + func ping() async throws { + guard let client = httpClient else { + throw EtcdError.notConnected + } + try await client.ping() + } + + // MARK: - Query Execution + + func execute(query: String) async throws -> PluginQueryResult { + let startTime = Date() + + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + + // Health monitor sends "SELECT 1" as a ping + if trimmed.lowercased() == "select 1" { + try await client.ping() + return PluginQueryResult( + columns: ["ok"], + columnTypeNames: ["Int32"], + rows: [["1"]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + // Check for tagged browsing queries + if EtcdQueryBuilder.isTaggedQuery(trimmed) { + return try await executeTaggedQuery(trimmed, client: client, startTime: startTime) + } + + let operation = try EtcdCommandParser.parse(trimmed) + return try await dispatch(operation, client: client, startTime: startTime) + } + + func executeParameterized(query: String, parameters: [String?]) async throws -> PluginQueryResult { + try await execute(query: query) + } + + func fetchRowCount(query: String) async throws -> Int { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + + if let parsed = EtcdQueryBuilder.parseRangeQuery(trimmed) { + return try await countKeys(prefix: parsed.prefix, filterType: parsed.filterType, filterValue: parsed.filterValue, client: client) + } + + if let parsed = EtcdQueryBuilder.parseCountQuery(trimmed) { + return try await countKeys(prefix: parsed.prefix, filterType: parsed.filterType, filterValue: parsed.filterValue, client: client) + } + + return 0 + } + + func fetchRows(query: String, offset: Int, limit: Int) async throws -> PluginQueryResult { + let startTime = Date() + + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + + if let parsed = EtcdQueryBuilder.parseRangeQuery(trimmed) { + return try await fetchKeysPage( + prefix: parsed.prefix, + offset: offset, + limit: limit, + sortAscending: parsed.sortAscending, + filterType: parsed.filterType, + filterValue: parsed.filterValue, + client: client, + startTime: startTime + ) + } + + return try await execute(query: query) + } + + // MARK: - Query Cancellation + + func cancelQuery() throws { + httpClient?.cancelCurrentRequest() + } + + func applyQueryTimeout(_ seconds: Int) async throws {} + + // MARK: - Schema Operations + + func fetchTables(schema: String?) async throws -> [PluginTableInfo] { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let prefix = _rootPrefix + let (b64Key, b64RangeEnd) = Self.allKeysRange(for: prefix) + + let response = try await client.rangeRequest(EtcdRangeRequest( + key: b64Key, + rangeEnd: b64RangeEnd, + limit: Int64(Self.maxKeys), + keysOnly: true + )) + + guard let kvs = response.kvs, !kvs.isEmpty else { + return [PluginTableInfo(name: "(root)", type: "PREFIX", rowCount: 0)] + } + + var prefixCounts: [String: Int] = [:] + var bareKeyCount = 0 + + for kv in kvs { + let key = EtcdHttpClient.base64Decode(kv.key) + let relative = stripRootPrefix(key) + + // Skip leading "/" when finding the first segment + let searchStart: String.Index + if relative.hasPrefix("/") && relative.count > 1 { + searchStart = relative.index(after: relative.startIndex) + } else { + searchStart = relative.startIndex + } + + if let slashIndex = relative[searchStart...].firstIndex(of: "/") { + // Include everything up to and including the slash (and leading / if present) + let segment = String(relative[relative.startIndex...slashIndex]) + prefixCounts[segment, default: 0] += 1 + } else { + bareKeyCount += 1 + } + } + + var tables: [PluginTableInfo] = [] + + if bareKeyCount > 0 { + tables.append(PluginTableInfo(name: "(root)", type: "PREFIX", rowCount: bareKeyCount)) + } + + for (prefixName, count) in prefixCounts.sorted(by: { $0.key < $1.key }) { + tables.append(PluginTableInfo(name: prefixName, type: "PREFIX", rowCount: count)) + } + + if tables.isEmpty { + tables.append(PluginTableInfo(name: "(root)", type: "PREFIX", rowCount: 0)) + } + + return tables + } + + func fetchColumns(table: String, schema: String?) async throws -> [PluginColumnInfo] { + [ + PluginColumnInfo(name: "Key", dataType: "String", isNullable: false, isPrimaryKey: true), + PluginColumnInfo(name: "Value", dataType: "String", isNullable: true), + PluginColumnInfo(name: "Version", dataType: "Int64", isNullable: false), + PluginColumnInfo(name: "ModRevision", dataType: "Int64", isNullable: false), + PluginColumnInfo(name: "CreateRevision", dataType: "Int64", isNullable: false), + PluginColumnInfo(name: "Lease", dataType: "String", isNullable: true), + ] + } + + func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] { + let tables = try await fetchTables(schema: schema) + let columns = try await fetchColumns(table: "", schema: schema) + var result: [String: [PluginColumnInfo]] = [:] + for table in tables { + result[table.name] = columns + } + return result + } + + func fetchIndexes(table: String, schema: String?) async throws -> [PluginIndexInfo] { + [] + } + + func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] { + [] + } + + func fetchApproximateRowCount(table: String, schema: String?) async throws -> Int? { + guard let client = httpClient else { + throw EtcdError.notConnected + } + let prefix = resolvedPrefix(for: table) + return try await countKeys(prefix: prefix, filterType: .none, filterValue: "", client: client) + } + + func fetchTableDDL(table: String, schema: String?) async throws -> String { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let prefix = resolvedPrefix(for: table) + let count = try await countKeys(prefix: prefix, filterType: .none, filterValue: "", client: client) + + return """ + // etcd key prefix: \(prefix.isEmpty ? "(all keys)" : prefix) + // Keys: \(count) + // Use 'get \(prefix.isEmpty ? "/" : prefix) --prefix' to browse keys + """ + } + + func fetchViewDefinition(view: String, schema: String?) async throws -> String { + "" + } + + func fetchTableMetadata(table: String, schema: String?) async throws -> PluginTableMetadata { + return PluginTableMetadata( + tableName: table, + engine: "etcd v3" + ) + } + + func fetchDatabases() async throws -> [String] { + ["default"] + } + + func fetchDatabaseMetadata(_ database: String) async throws -> PluginDatabaseMetadata { + guard let client = httpClient else { + throw EtcdError.notConnected + } + + let status = try await client.endpointStatus() + let dbSizeBytes = Int64(status.dbSize ?? "0") + return PluginDatabaseMetadata( + name: database, + sizeBytes: dbSizeBytes + ) + } + + // MARK: - NoSQL Query Building Hooks + + func buildBrowseQuery( + table: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + columns: [String], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildBrowseQuery( + prefix: prefix, sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + func buildFilteredQuery( + table: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + columns: [String], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildFilteredQuery( + prefix: prefix, filters: filters, logicMode: logicMode, + sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + func buildQuickSearchQuery( + table: String, + searchText: String, + columns: [String], + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildQuickSearchQuery( + prefix: prefix, searchText: searchText, + sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + func buildCombinedQuery( + table: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + searchText: String, + searchColumns: [String], + sortColumns: [(columnIndex: Int, ascending: Bool)], + columns: [String], + limit: Int, + offset: Int + ) -> String? { + let prefix = resolvedPrefix(for: table) + return EtcdQueryBuilder().buildCombinedQuery( + prefix: prefix, filters: filters, logicMode: logicMode, + searchText: searchText, sortColumns: sortColumns, limit: limit, offset: offset + ) + } + + // MARK: - Statement Generation + + func generateStatements( + table: String, + columns: [String], + changes: [PluginRowChange], + insertedRowData: [Int: [String?]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [(statement: String, parameters: [String?])]? { + let generator = EtcdStatementGenerator( + prefix: resolvedPrefix(for: table), + columns: columns + ) + return generator.generateStatements( + from: changes, + insertedRowData: insertedRowData, + deletedRowIndices: deletedRowIndices, + insertedRowIndices: insertedRowIndices + ) + } + + func allTablesMetadataSQL(schema: String?) -> String? { + let prefix = _rootPrefix + return "get \(escapeArgument(prefix.isEmpty ? "/" : prefix)) --prefix --keys-only" + } + + // MARK: - Command Dispatch + + private func dispatch( + _ operation: EtcdOperation, + client: EtcdHttpClient, + startTime: Date + ) async throws -> PluginQueryResult { + switch operation { + case .get(let key, let prefix, let limit, let keysOnly, let sortOrder, let sortTarget): + return try await dispatchGet( + key: key, prefix: prefix, limit: limit, keysOnly: keysOnly, + sortOrder: sortOrder, sortTarget: sortTarget, client: client, startTime: startTime + ) + + case .put(let key, let value, let leaseId): + return try await dispatchPut(key: key, value: value, leaseId: leaseId, client: client, startTime: startTime) + + case .del(let key, let prefix): + return try await dispatchDel(key: key, prefix: prefix, client: client, startTime: startTime) + + case .watch(let key, let prefix, let timeout): + return try await dispatchWatch(key: key, prefix: prefix, timeout: timeout, client: client, startTime: startTime) + + case .leaseGrant(let ttl): + return try await dispatchLeaseGrant(ttl: ttl, client: client, startTime: startTime) + + case .leaseRevoke(let leaseId): + return try await dispatchLeaseRevoke(leaseId: leaseId, client: client, startTime: startTime) + + case .leaseTimetolive(let leaseId, let keys): + return try await dispatchLeaseTimetolive(leaseId: leaseId, keys: keys, client: client, startTime: startTime) + + case .leaseList: + return try await dispatchLeaseList(client: client, startTime: startTime) + + case .leaseKeepAlive(let leaseId): + return try await dispatchLeaseKeepAlive(leaseId: leaseId, client: client, startTime: startTime) + + case .memberList: + return try await dispatchMemberList(client: client, startTime: startTime) + + case .endpointStatus: + return try await dispatchEndpointStatus(client: client, startTime: startTime) + + case .endpointHealth: + return try await dispatchEndpointHealth(client: client, startTime: startTime) + + case .compaction(let revision, let physical): + try await client.compaction(revision: revision, physical: physical) + return PluginQueryResult( + columns: ["Result"], + columnTypeNames: ["String"], + rows: [["Compaction completed at revision \(revision)"]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + + case .authEnable: + try await client.authEnable() + return singleMessageResult("Authentication enabled", startTime: startTime) + + case .authDisable: + try await client.authDisable() + return singleMessageResult("Authentication disabled", startTime: startTime) + + case .userAdd(let name, let password): + try await client.userAdd(name: name, password: password ?? "") + return singleMessageResult("User '\(name)' added", startTime: startTime) + + case .userDelete(let name): + try await client.userDelete(name: name) + return singleMessageResult("User '\(name)' deleted", startTime: startTime) + + case .userList: + let users = try await client.userList() + let rows = users.map { [$0 as String?] } + return PluginQueryResult( + columns: ["User"], + columnTypeNames: ["String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + + case .roleAdd(let name): + try await client.roleAdd(name: name) + return singleMessageResult("Role '\(name)' added", startTime: startTime) + + case .roleDelete(let name): + try await client.roleDelete(name: name) + return singleMessageResult("Role '\(name)' deleted", startTime: startTime) + + case .roleList: + let roles = try await client.roleList() + let rows = roles.map { [$0 as String?] } + return PluginQueryResult( + columns: ["Role"], + columnTypeNames: ["String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + + case .userGrantRole(let user, let role): + try await client.userGrantRole(user: user, role: role) + return singleMessageResult("Role '\(role)' granted to user '\(user)'", startTime: startTime) + + case .userRevokeRole(let user, let role): + try await client.userRevokeRole(user: user, role: role) + return singleMessageResult("Role '\(role)' revoked from user '\(user)'", startTime: startTime) + + case .unknown(let command, let args): + Self.logger.warning("Unknown etcd command: \(command) \(args.joined(separator: " "))") + throw EtcdParseError.unknownCommand(command) + } + } + + // MARK: - KV Dispatch + + private func dispatchGet( + key: String, prefix: Bool, limit: Int64?, keysOnly: Bool, + sortOrder: EtcdSortOrder?, sortTarget: EtcdSortTarget?, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let b64Key = EtcdHttpClient.base64Encode(key) + var req = EtcdRangeRequest(key: b64Key) + + if prefix { + req.rangeEnd = EtcdHttpClient.base64Encode(EtcdHttpClient.prefixRangeEnd(for: key)) + } + if let limit = limit { + req.limit = limit + } + if keysOnly { + req.keysOnly = true + } + if let order = sortOrder { + req.sortOrder = order.rawValue + } + if let target = sortTarget { + req.sortTarget = target.rawValue + } + + let response = try await client.rangeRequest(req) + + if keysOnly { + let rows: [[String?]] = (response.kvs ?? []).map { kv in + [EtcdHttpClient.base64Decode(kv.key)] + } + return PluginQueryResult( + columns: ["Key"], + columnTypeNames: ["String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + return mapKvsToResult(response.kvs ?? [], startTime: startTime) + } + + private func dispatchPut( + key: String, value: String, leaseId: Int64?, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + var req = EtcdPutRequest( + key: EtcdHttpClient.base64Encode(key), + value: EtcdHttpClient.base64Encode(value), + prevKv: true + ) + if let leaseId = leaseId { + req.lease = String(leaseId) + } + + let response = try await client.putRequest(req) + let revision = response.header?.revision ?? "unknown" + + return PluginQueryResult( + columns: ["Key", "Value", "Revision"], + columnTypeNames: ["String", "String", "Int64"], + rows: [[key, value, revision]], + rowsAffected: 1, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchDel( + key: String, prefix: Bool, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + var req = EtcdDeleteRequest( + key: EtcdHttpClient.base64Encode(key), + prevKv: true + ) + if prefix { + req.rangeEnd = EtcdHttpClient.base64Encode(EtcdHttpClient.prefixRangeEnd(for: key)) + } + + let response = try await client.deleteRequest(req) + let deleted = response.deleted ?? "0" + + return PluginQueryResult( + columns: ["Deleted"], + columnTypeNames: ["Int64"], + rows: [[deleted]], + rowsAffected: Int(deleted) ?? 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + // MARK: - Watch Dispatch + + private func dispatchWatch( + key: String, prefix: Bool, timeout: TimeInterval, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let events = try await client.watch(key: key, prefix: prefix, timeout: timeout) + + let rows: [[String?]] = events.map { event in + let eventType = event.type ?? "UNKNOWN" + let eventKey = event.kv.map { EtcdHttpClient.base64Decode($0.key) } ?? "" + let eventValue = event.kv?.value.map { EtcdHttpClient.base64Decode($0) } ?? "" + let modRevision = event.kv?.modRevision ?? "" + let prevValue = event.prevKv?.value.map { EtcdHttpClient.base64Decode($0) } ?? "" + return [eventType, eventKey, eventValue, modRevision, prevValue] + } + + return PluginQueryResult( + columns: ["Type", "Key", "Value", "ModRevision", "PrevValue"], + columnTypeNames: ["String", "String", "String", "Int64", "String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + // MARK: - Lease Dispatch + + private func dispatchLeaseGrant( + ttl: Int64, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.leaseGrant(ttl: ttl) + let leaseIdStr = response.ID ?? "unknown" + let grantedTtl = response.TTL ?? String(ttl) + + let hexId: String + if let idNum = Int64(leaseIdStr) { + hexId = String(idNum, radix: 16) + } else { + hexId = leaseIdStr + } + + return PluginQueryResult( + columns: ["LeaseID", "LeaseID (hex)", "TTL"], + columnTypeNames: ["String", "String", "Int64"], + rows: [[leaseIdStr, hexId, grantedTtl]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchLeaseRevoke( + leaseId: Int64, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + try await client.leaseRevoke(leaseId: leaseId) + let hexId = String(leaseId, radix: 16) + return singleMessageResult("Lease \(hexId) revoked", startTime: startTime) + } + + private func dispatchLeaseTimetolive( + leaseId: Int64, keys: Bool, + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.leaseTimeToLive(leaseId: leaseId, keys: keys) + + let idStr = response.ID ?? String(leaseId) + let hexId: String + if let idNum = Int64(idStr) { + hexId = String(idNum, radix: 16) + } else { + hexId = idStr + } + + let ttl = response.TTL ?? "unknown" + let grantedTtl = response.grantedTTL ?? "unknown" + let attachedKeys = (response.keys ?? []) + .map { EtcdHttpClient.base64Decode($0) } + .joined(separator: ", ") + + return PluginQueryResult( + columns: ["LeaseID (hex)", "TTL", "GrantedTTL", "AttachedKeys"], + columnTypeNames: ["String", "Int64", "Int64", "String"], + rows: [[hexId, ttl, grantedTtl, attachedKeys]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchLeaseList( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.leaseList() + let rows: [[String?]] = (response.leases ?? []).map { lease in + let idStr = lease.ID + let hexId: String + if let idNum = Int64(idStr) { + hexId = String(idNum, radix: 16) + } else { + hexId = idStr + } + return [idStr, hexId] + } + + return PluginQueryResult( + columns: ["LeaseID", "LeaseID (hex)"], + columnTypeNames: ["String", "String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchLeaseKeepAlive( + leaseId: Int64, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + // lease keep-alive requires a streaming gRPC connection not available via HTTP gateway. + // Show the current TTL instead so the user can see the lease status. + let response = try await client.leaseTimeToLive(leaseId: leaseId, keys: false) + let ttl = response.TTL ?? "unknown" + let hexId = String(leaseId, radix: 16) + return singleMessageResult("Lease \(hexId) current TTL: \(ttl)s (keep-alive requires streaming; use etcdctl CLI for persistent keep-alive)", startTime: startTime) + } + + // MARK: - Cluster Dispatch + + private func dispatchMemberList( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let response = try await client.memberList() + let rows: [[String?]] = (response.members ?? []).map { member in + let id = member.ID ?? "unknown" + let hexId: String + if let idNum = UInt64(id) { + hexId = String(idNum, radix: 16) + } else { + hexId = id + } + let name = member.name ?? "" + let peerUrls = (member.peerURLs ?? []).joined(separator: ", ") + let clientUrls = (member.clientURLs ?? []).joined(separator: ", ") + let isLearner = member.isLearner == true ? "true" : "false" + return [hexId, name, peerUrls, clientUrls, isLearner] + } + + return PluginQueryResult( + columns: ["ID", "Name", "PeerURLs", "ClientURLs", "IsLearner"], + columnTypeNames: ["String", "String", "String", "String", "String"], + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchEndpointStatus( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + let status = try await client.endpointStatus() + + let version = status.version ?? "unknown" + let dbSize = status.dbSize ?? "unknown" + let leader = status.leader ?? "unknown" + let raftIndex = status.raftIndex ?? "unknown" + let raftTerm = status.raftTerm ?? "unknown" + let errors = (status.errors ?? []).joined(separator: "; ") + + return PluginQueryResult( + columns: ["Version", "DbSize", "Leader", "RaftIndex", "RaftTerm", "Errors"], + columnTypeNames: ["String", "String", "String", "String", "String", "String"], + rows: [[version, dbSize, leader, raftIndex, raftTerm, errors.isEmpty ? nil : errors]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func dispatchEndpointHealth( + client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + try await client.ping() + return singleMessageResult("endpoint is healthy", startTime: startTime) + } + + // MARK: - Tagged Query Execution + + private func executeTaggedQuery( + _ query: String, client: EtcdHttpClient, startTime: Date + ) async throws -> PluginQueryResult { + if let parsed = EtcdQueryBuilder.parseRangeQuery(query) { + return try await fetchKeysPage( + prefix: parsed.prefix, + offset: parsed.offset, + limit: parsed.limit, + sortAscending: parsed.sortAscending, + filterType: parsed.filterType, + filterValue: parsed.filterValue, + client: client, + startTime: startTime + ) + } + + if let parsed = EtcdQueryBuilder.parseCountQuery(query) { + let count = try await countKeys(prefix: parsed.prefix, filterType: parsed.filterType, filterValue: parsed.filterValue, client: client) + return PluginQueryResult( + columns: ["Count"], + columnTypeNames: ["Int64"], + rows: [[String(count)]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + throw EtcdError.serverError("Invalid tagged query format") + } + + // MARK: - Key Fetching + + private func fetchKeysPage( + prefix: String, + offset: Int, + limit: Int, + sortAscending: Bool, + filterType: EtcdFilterType, + filterValue: String, + client: EtcdHttpClient, + startTime: Date + ) async throws -> PluginQueryResult { + let (b64Key, b64RangeEnd) = Self.allKeysRange(for: prefix) + + let needsClientFilter = filterType != .none + + // Fetch enough keys to cover offset + limit + client filtering + let fetchLimit = needsClientFilter ? Int64(Self.maxKeys) : Int64(min(offset + limit, Self.maxKeys)) + + var req = EtcdRangeRequest(key: b64Key, rangeEnd: b64RangeEnd, limit: fetchLimit) + req.sortOrder = sortAscending ? "ASCEND" : "DESCEND" + req.sortTarget = "KEY" + + let response = try await client.rangeRequest(req) + var kvs = response.kvs ?? [] + + // Apply client-side filter if needed + if needsClientFilter { + kvs = kvs.filter { kv in + let key = EtcdHttpClient.base64Decode(kv.key) + return matchesFilter(key: key, filterType: filterType, filterValue: filterValue) + } + } + + // Apply pagination + let total = kvs.count + guard offset < total else { + return emptyResult(startTime: startTime) + } + let pageEnd = min(offset + limit, total) + let pageKvs = Array(kvs[offset ..< pageEnd]) + + return mapKvsToResult(pageKvs, startTime: startTime) + } + + private func countKeys( + prefix: String, + filterType: EtcdFilterType, + filterValue: String, + client: EtcdHttpClient + ) async throws -> Int { + let (b64Key, b64RangeEnd) = Self.allKeysRange(for: prefix) + + if filterType == .none { + var req = EtcdRangeRequest(key: b64Key, rangeEnd: b64RangeEnd, limit: Int64(Self.maxKeys)) + req.countOnly = true + let response = try await client.rangeRequest(req) + return Int(response.count ?? "0") ?? 0 + } + + // Need to fetch keys and filter client-side + let req = EtcdRangeRequest(key: b64Key, rangeEnd: b64RangeEnd, limit: Int64(Self.maxKeys), keysOnly: true) + let response = try await client.rangeRequest(req) + let kvs = response.kvs ?? [] + + return kvs.filter { kv in + let key = EtcdHttpClient.base64Decode(kv.key) + return matchesFilter(key: key, filterType: filterType, filterValue: filterValue) + }.count + } + + // MARK: - Helpers + + /// Returns (base64Key, base64RangeEnd) for a prefix range query. + /// Empty prefix uses null byte (\0) as key to mean "all keys". + private static func allKeysRange(for prefix: String) -> (key: String, rangeEnd: String) { + if prefix.isEmpty { + // \0 as key = start from beginning, \0 as range_end = all keys + let b64Key = EtcdHttpClient.base64Encode("\0") + let b64RangeEnd = EtcdHttpClient.base64Encode("\0") + return (b64Key, b64RangeEnd) + } + let b64Key = EtcdHttpClient.base64Encode(prefix) + let b64RangeEnd = EtcdHttpClient.base64Encode(EtcdHttpClient.prefixRangeEnd(for: prefix)) + return (b64Key, b64RangeEnd) + } + + private func resolvedPrefix(for table: String) -> String { + if table == "(root)" { + return _rootPrefix + } + if _rootPrefix.isEmpty { + return table + } + let root = _rootPrefix.hasSuffix("/") ? _rootPrefix : _rootPrefix + "/" + return root + table + } + + private func stripRootPrefix(_ key: String) -> String { + guard !_rootPrefix.isEmpty else { return key } + let root = _rootPrefix.hasSuffix("/") ? _rootPrefix : _rootPrefix + "/" + if key.hasPrefix(root) { + return String(key.dropFirst(root.count)) + } + return key + } + + private func matchesFilter(key: String, filterType: EtcdFilterType, filterValue: String) -> Bool { + switch filterType { + case .none: + return true + case .contains: + return key.localizedCaseInsensitiveContains(filterValue) + case .startsWith: + return key.lowercased().hasPrefix(filterValue.lowercased()) + case .endsWith: + return key.lowercased().hasSuffix(filterValue.lowercased()) + case .equals: + return key == filterValue + } + } + + private func mapKvsToResult(_ kvs: [EtcdKeyValue], startTime: Date) -> PluginQueryResult { + let rows: [[String?]] = kvs.map { kv in + let key = EtcdHttpClient.base64Decode(kv.key) + let value = kv.value.map { EtcdHttpClient.base64Decode($0) } + let version = kv.version ?? "0" + let modRevision = kv.modRevision ?? "0" + let createRevision = kv.createRevision ?? "0" + let lease = kv.lease ?? "0" + let leaseDisplay = lease == "0" ? "" : formatLeaseHex(lease) + return [key, value, version, modRevision, createRevision, leaseDisplay] + } + + return PluginQueryResult( + columns: Self.columns, + columnTypeNames: Self.columnTypeNames, + rows: rows, + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func emptyResult(startTime: Date) -> PluginQueryResult { + PluginQueryResult( + columns: Self.columns, + columnTypeNames: Self.columnTypeNames, + rows: [], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func singleMessageResult(_ message: String, startTime: Date) -> PluginQueryResult { + PluginQueryResult( + columns: ["Result"], + columnTypeNames: ["String"], + rows: [[message]], + rowsAffected: 0, + executionTime: Date().timeIntervalSince(startTime) + ) + } + + private func formatLeaseHex(_ leaseStr: String) -> String { + if let leaseNum = Int64(leaseStr) { + return String(leaseNum, radix: 16) + } + return leaseStr + } + + private func escapeArgument(_ value: String) -> String { + let needsQuoting = value.isEmpty || value.contains(where: { $0.isWhitespace || $0 == "\"" || $0 == "'" }) + if needsQuoting { + let escaped = value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + return "\"\(escaped)\"" + } + return value + } +} diff --git a/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift b/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift new file mode 100644 index 00000000..ce4d9a93 --- /dev/null +++ b/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift @@ -0,0 +1,202 @@ +// +// EtcdQueryBuilder.swift +// EtcdDriverPlugin +// +// Builds internal query strings for etcd key browsing and filtering. +// + +import Foundation +import TableProPluginKit + +enum EtcdFilterType: String { + case none + case contains + case startsWith + case endsWith + case equals +} + +struct EtcdParsedQuery { + let prefix: String + let limit: Int + let offset: Int + let sortAscending: Bool + let filterType: EtcdFilterType + let filterValue: String +} + +struct EtcdParsedCountQuery { + let prefix: String + let filterType: EtcdFilterType + let filterValue: String +} + +struct EtcdQueryBuilder { + static let rangeTag = "ETCD_RANGE:" + static let countTag = "ETCD_COUNT:" + + func buildBrowseQuery( + prefix: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: .none, filterValue: "" + ) + } + + func buildFilteredQuery( + prefix: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + let (filterType, filterValue) = extractKeyFilter(from: filters) + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: filterType, filterValue: filterValue + ) + } + + func buildQuickSearchQuery( + prefix: String, + searchText: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: .contains, filterValue: searchText + ) + } + + func buildCombinedQuery( + prefix: String, + filters: [(column: String, op: String, value: String)], + logicMode: String, + searchText: String, + sortColumns: [(columnIndex: Int, ascending: Bool)], + limit: Int, + offset: Int + ) -> String { + let sortAsc = sortColumns.first?.ascending ?? true + if !searchText.isEmpty { + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: .contains, filterValue: searchText + ) + } + let (filterType, filterValue) = extractKeyFilter(from: filters) + return Self.encodeRangeQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAsc, filterType: filterType, filterValue: filterValue + ) + } + + func buildCountQuery(prefix: String) -> String { + Self.encodeCountQuery(prefix: prefix, filterType: .none, filterValue: "") + } + + // MARK: - Encoding + + private static func encodeRangeQuery( + prefix: String, limit: Int, offset: Int, + sortAscending: Bool, filterType: EtcdFilterType, filterValue: String + ) -> String { + let b64Prefix = Data(prefix.utf8).base64EncodedString() + let b64Filter = Data(filterValue.utf8).base64EncodedString() + return "\(rangeTag)\(b64Prefix):\(limit):\(offset):\(sortAscending ? "1" : "0"):\(filterType.rawValue):\(b64Filter)" + } + + private static func encodeCountQuery( + prefix: String, filterType: EtcdFilterType, filterValue: String + ) -> String { + let b64Prefix = Data(prefix.utf8).base64EncodedString() + let b64Filter = Data(filterValue.utf8).base64EncodedString() + return "\(countTag)\(b64Prefix):\(filterType.rawValue):\(b64Filter)" + } + + // MARK: - Decoding + + static func parseRangeQuery(_ query: String) -> EtcdParsedQuery? { + guard query.hasPrefix(rangeTag) else { return nil } + let body = String(query.dropFirst(rangeTag.count)) + let parts = body.components(separatedBy: ":") + guard parts.count >= 6 else { return nil } + + guard let prefixData = Data(base64Encoded: parts[0]), + let prefix = String(data: prefixData, encoding: .utf8), + let limit = Int(parts[1]), + let offset = Int(parts[2]) else { return nil } + + let sortAscending = parts[3] == "1" + let filterType = EtcdFilterType(rawValue: parts[4]) ?? .none + + let filterB64 = parts[5...].joined(separator: ":") + let filterValue: String + if let filterData = Data(base64Encoded: filterB64), + let decoded = String(data: filterData, encoding: .utf8) { + filterValue = decoded + } else { + filterValue = "" + } + + return EtcdParsedQuery( + prefix: prefix, limit: limit, offset: offset, + sortAscending: sortAscending, filterType: filterType, filterValue: filterValue + ) + } + + static func parseCountQuery(_ query: String) -> EtcdParsedCountQuery? { + guard query.hasPrefix(countTag) else { return nil } + let body = String(query.dropFirst(countTag.count)) + let parts = body.components(separatedBy: ":") + guard parts.count >= 3 else { return nil } + + guard let prefixData = Data(base64Encoded: parts[0]), + let prefix = String(data: prefixData, encoding: .utf8) else { return nil } + + let filterType = EtcdFilterType(rawValue: parts[1]) ?? .none + let filterB64 = parts[2...].joined(separator: ":") + let filterValue: String + if let filterData = Data(base64Encoded: filterB64), + let decoded = String(data: filterData, encoding: .utf8) { + filterValue = decoded + } else { + filterValue = "" + } + + return EtcdParsedCountQuery( + prefix: prefix, filterType: filterType, filterValue: filterValue + ) + } + + static func isTaggedQuery(_ query: String) -> Bool { + query.hasPrefix(rangeTag) || query.hasPrefix(countTag) + } + + // MARK: - Filter Extraction + + private func extractKeyFilter( + from filters: [(column: String, op: String, value: String)] + ) -> (EtcdFilterType, String) { + let keyFilters = filters.filter { $0.column == "Key" } + guard let filter = keyFilters.first else { return (.none, "") } + + switch filter.op { + case "CONTAINS": return (.contains, filter.value) + case "STARTS WITH": return (.startsWith, filter.value) + case "ENDS WITH": return (.endsWith, filter.value) + case "=": return (.equals, filter.value) + default: return (.none, "") + } + } +} diff --git a/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift b/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift new file mode 100644 index 00000000..1a91bd72 --- /dev/null +++ b/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift @@ -0,0 +1,153 @@ +// +// EtcdStatementGenerator.swift +// EtcdDriverPlugin +// +// Generates etcdctl commands from tracked cell changes. +// + +import Foundation +import os +import TableProPluginKit + +struct EtcdStatementGenerator { + private static let logger = Logger(subsystem: "com.TablePro.EtcdDriver", category: "EtcdStatementGenerator") + + let prefix: String + let columns: [String] + + var keyColumnIndex: Int? { columns.firstIndex(of: "Key") } + private var valueColumnIndex: Int? { columns.firstIndex(of: "Value") } + private var leaseColumnIndex: Int? { columns.firstIndex(of: "Lease") } + + func generateStatements( + from changes: [PluginRowChange], + insertedRowData: [Int: [String?]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [(statement: String, parameters: [String?])] { + var statements: [(statement: String, parameters: [String?])] = [] + + for change in changes { + switch change.type { + case .insert: + guard insertedRowIndices.contains(change.rowIndex) else { continue } + statements += generateInsert(for: change, insertedRowData: insertedRowData) + case .update: + statements += generateUpdate(for: change) + case .delete: + guard deletedRowIndices.contains(change.rowIndex) else { continue } + if let key = extractKey(from: change) { + statements.append((statement: "del \(escapeArgument(key))", parameters: [])) + } + } + } + + return statements + } + + private func generateInsert( + for change: PluginRowChange, + insertedRowData: [Int: [String?]] + ) -> [(statement: String, parameters: [String?])] { + var key: String? + var value: String? + var leaseId: String? + + if let values = insertedRowData[change.rowIndex] { + if let ki = keyColumnIndex, ki < values.count { key = values[ki] } + if let vi = valueColumnIndex, vi < values.count { value = values[vi] } + if let li = leaseColumnIndex, li < values.count { leaseId = values[li] } + } else { + for cellChange in change.cellChanges { + switch cellChange.columnName { + case "Key": key = cellChange.newValue + case "Value": value = cellChange.newValue + case "Lease": leaseId = cellChange.newValue + default: break + } + } + } + + guard let k = key, !k.isEmpty else { + Self.logger.warning("Skipping INSERT - no key provided") + return [] + } + + let v = value ?? "" + var cmd = "put \(escapeArgument(k)) \(escapeArgument(v))" + if let lease = leaseId, !lease.isEmpty, lease != "0" { + cmd += " --lease=\(lease)" + } + + return [(statement: cmd, parameters: [])] + } + + private func generateUpdate( + for change: PluginRowChange + ) -> [(statement: String, parameters: [String?])] { + guard !change.cellChanges.isEmpty else { return [] } + guard let originalKey = extractKey(from: change) else { + Self.logger.warning("Skipping UPDATE - no original key") + return [] + } + + var statements: [(statement: String, parameters: [String?])] = [] + + let keyChange = change.cellChanges.first { $0.columnName == "Key" } + let newKey = keyChange?.newValue ?? originalKey + + if newKey != originalKey { + statements.append((statement: "del \(escapeArgument(originalKey))", parameters: [])) + } + + let valueChange = change.cellChanges.first { $0.columnName == "Value" } + let leaseChange = change.cellChanges.first { $0.columnName == "Lease" } + + if valueChange != nil || newKey != originalKey { + let newValue = valueChange?.newValue ?? extractOriginalValue(from: change) ?? "" + var cmd = "put \(escapeArgument(newKey)) \(escapeArgument(newValue))" + if let lease = leaseChange?.newValue, !lease.isEmpty, lease != "0" { + cmd += " --lease=\(lease)" + } + statements.append((statement: cmd, parameters: [])) + } else if let lease = leaseChange?.newValue { + let currentValue = extractOriginalValue(from: change) ?? "" + var cmd = "put \(escapeArgument(newKey)) \(escapeArgument(currentValue))" + if !lease.isEmpty && lease != "0" { + cmd += " --lease=\(lease)" + } + statements.append((statement: cmd, parameters: [])) + } + + return statements + } + + // MARK: - Helpers + + private func extractKey(from change: PluginRowChange) -> String? { + guard let keyIndex = keyColumnIndex, + let originalRow = change.originalRow, + keyIndex < originalRow.count else { return nil } + return originalRow[keyIndex] + } + + private func extractOriginalValue(from change: PluginRowChange) -> String? { + guard let valueIndex = valueColumnIndex, + let originalRow = change.originalRow, + valueIndex < originalRow.count else { return nil } + return originalRow[valueIndex] + } + + private func escapeArgument(_ value: String) -> String { + let needsQuoting = value.isEmpty || value.contains(where: { $0.isWhitespace || $0 == "\"" || $0 == "'" }) + if needsQuoting { + let escaped = value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\r", with: "\\r") + return "\"\(escaped)\"" + } + return value + } +} diff --git a/Plugins/EtcdDriverPlugin/Info.plist b/Plugins/EtcdDriverPlugin/Info.plist new file mode 100644 index 00000000..12b650a8 --- /dev/null +++ b/Plugins/EtcdDriverPlugin/Info.plist @@ -0,0 +1,8 @@ + + + + + TableProPluginKitVersion + 1 + + diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index 5c1537bd..7a32062a 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -35,6 +35,14 @@ 5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000007 /* CodeEditTextView */; }; 5ACE00012F4F00000000000A /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000009 /* Sparkle */; }; 5ACE00012F4F00000000000D /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000C /* MarkdownUI */; }; + 5AEA8B302F6808270040461A /* EtcdDriverPlugin.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5AEA8B2A2F6808270040461A /* EtcdDriverPlugin.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5AEA8B422F6808CA0040461A /* EtcdStatementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B402F6808CA0040461A /* EtcdStatementGenerator.swift */; }; + 5AEA8B432F6808CA0040461A /* EtcdPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3D2F6808CA0040461A /* EtcdPlugin.swift */; }; + 5AEA8B442F6808CA0040461A /* EtcdCommandParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3B2F6808CA0040461A /* EtcdCommandParser.swift */; }; + 5AEA8B452F6808CA0040461A /* EtcdPluginDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3E2F6808CA0040461A /* EtcdPluginDriver.swift */; }; + 5AEA8B462F6808CA0040461A /* EtcdQueryBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3F2F6808CA0040461A /* EtcdQueryBuilder.swift */; }; + 5AEA8B472F6808CA0040461A /* EtcdHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3C2F6808CA0040461A /* EtcdHttpClient.swift */; }; + 5AEA8B492F6808E90040461A /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; }; 5AEE5B362F5C9B7B00FA84D7 /* OracleNIO in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000F /* OracleNIO */; }; /* End PBXBuildFile section */ @@ -109,6 +117,13 @@ remoteGlobalIDString = 5A86C000000000000; remoteInfo = SQLExport; }; + 5AEA8B312F6808270040461A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5AEA8B292F6808270040461A; + remoteInfo = EtcdDriverPlugin; + }; 5ABCC5AB2F43856700EAF3FC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */; @@ -131,6 +146,7 @@ 5A86A000D00000000 /* CSVExport.tableplugin in Copy Plug-Ins */, 5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */, 5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */, + 5AEA8B302F6808270040461A /* EtcdDriverPlugin.tableplugin in Copy Plug-Ins */, ); name = "Copy Plug-Ins"; runOnlyForDeploymentPostprocessing = 0; @@ -168,6 +184,13 @@ 5A86F000100000000 /* SQLImport.tableplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SQLImport.tableplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 5A87A000100000000 /* CassandraDriver.tableplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CassandraDriver.tableplugin; sourceTree = BUILT_PRODUCTS_DIR; }; 5ABCC5A72F43856700EAF3FC /* TableProTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TableProTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5AEA8B2A2F6808270040461A /* EtcdDriverPlugin.tableplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EtcdDriverPlugin.tableplugin; sourceTree = BUILT_PRODUCTS_DIR; }; + 5AEA8B3B2F6808CA0040461A /* EtcdCommandParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtcdCommandParser.swift; sourceTree = ""; }; + 5AEA8B3C2F6808CA0040461A /* EtcdHttpClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtcdHttpClient.swift; sourceTree = ""; }; + 5AEA8B3D2F6808CA0040461A /* EtcdPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtcdPlugin.swift; sourceTree = ""; }; + 5AEA8B3E2F6808CA0040461A /* EtcdPluginDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtcdPluginDriver.swift; sourceTree = ""; }; + 5AEA8B3F2F6808CA0040461A /* EtcdQueryBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtcdQueryBuilder.swift; sourceTree = ""; }; + 5AEA8B402F6808CA0040461A /* EtcdStatementGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtcdStatementGenerator.swift; sourceTree = ""; }; 5ASECRETS000000000000001 /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -608,6 +631,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5AEA8B272F6808270040461A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5AEA8B492F6808E90040461A /* TableProPluginKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -622,6 +653,7 @@ 5A1091BE2EF17EDC0055EA7C = { isa = PBXGroup; children = ( + 5AEA8B412F6808CA0040461A /* EtcdDriverPlugin */, 5A1091C92EF17EDC0055EA7C /* TablePro */, 5A860000500000000 /* Plugins/TableProPluginKit */, 5A861000500000000 /* Plugins/OracleDriverPlugin */, @@ -643,6 +675,7 @@ 5ABCC5A82F43856700EAF3FC /* TableProTests */, 5A1091C82EF17EDC0055EA7C /* Products */, 5A05FBC72F3EDF7500819CD7 /* Recovered References */, + 5AEA8B482F6808E90040461A /* Frameworks */, ); sourceTree = ""; }; @@ -668,10 +701,31 @@ 5A86E000100000000 /* MQLExport.tableplugin */, 5A86F000100000000 /* SQLImport.tableplugin */, 5ABCC5A72F43856700EAF3FC /* TableProTests.xctest */, + 5AEA8B2A2F6808270040461A /* EtcdDriverPlugin.tableplugin */, ); name = Products; sourceTree = ""; }; + 5AEA8B412F6808CA0040461A /* EtcdDriverPlugin */ = { + isa = PBXGroup; + children = ( + 5AEA8B3B2F6808CA0040461A /* EtcdCommandParser.swift */, + 5AEA8B3C2F6808CA0040461A /* EtcdHttpClient.swift */, + 5AEA8B3D2F6808CA0040461A /* EtcdPlugin.swift */, + 5AEA8B3E2F6808CA0040461A /* EtcdPluginDriver.swift */, + 5AEA8B3F2F6808CA0040461A /* EtcdQueryBuilder.swift */, + 5AEA8B402F6808CA0040461A /* EtcdStatementGenerator.swift */, + ); + path = EtcdDriverPlugin; + sourceTree = ""; + }; + 5AEA8B482F6808E90040461A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -708,6 +762,7 @@ 5A86A000C00000000 /* PBXTargetDependency */, 5A86B000C00000000 /* PBXTargetDependency */, 5A86C000C00000000 /* PBXTargetDependency */, + 5AEA8B322F6808270040461A /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 5A1091C92EF17EDC0055EA7C /* TablePro */, @@ -1089,6 +1144,25 @@ productReference = 5ABCC5A72F43856700EAF3FC /* TableProTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 5AEA8B292F6808270040461A /* EtcdDriverPlugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5AEA8B2D2F6808270040461A /* Build configuration list for PBXNativeTarget "EtcdDriverPlugin" */; + buildPhases = ( + 5AEA8B262F6808270040461A /* Sources */, + 5AEA8B272F6808270040461A /* Frameworks */, + 5AEA8B282F6808270040461A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = EtcdDriverPlugin; + packageProductDependencies = ( + ); + productName = EtcdDriverPlugin; + productReference = 5AEA8B2A2F6808270040461A /* EtcdDriverPlugin.tableplugin */; + productType = "com.apple.product-type.bundle"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1151,6 +1225,10 @@ CreatedOnToolsVersion = 26.2; TestTargetID = 5A1091C62EF17EDC0055EA7C; }; + 5AEA8B292F6808270040461A = { + CreatedOnToolsVersion = 26.3; + LastSwiftMigration = 2630; + }; }; }; buildConfigurationList = 5A1091C22EF17EDC0055EA7C /* Build configuration list for PBXProject "TablePro" */; @@ -1195,6 +1273,7 @@ 5A86E000000000000 /* MQLExport */, 5A86F000000000000 /* SQLImport */, 5ABCC5A62F43856700EAF3FC /* TableProTests */, + 5AEA8B292F6808270040461A /* EtcdDriverPlugin */, ); }; /* End PBXProject section */ @@ -1333,6 +1412,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5AEA8B282F6808270040461A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1469,6 +1555,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5AEA8B262F6808270040461A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5AEA8B422F6808CA0040461A /* EtcdStatementGenerator.swift in Sources */, + 5AEA8B432F6808CA0040461A /* EtcdPlugin.swift in Sources */, + 5AEA8B442F6808CA0040461A /* EtcdCommandParser.swift in Sources */, + 5AEA8B452F6808CA0040461A /* EtcdPluginDriver.swift in Sources */, + 5AEA8B462F6808CA0040461A /* EtcdQueryBuilder.swift in Sources */, + 5AEA8B472F6808CA0040461A /* EtcdHttpClient.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1522,6 +1621,11 @@ target = 5A86C000000000000 /* SQLExport */; targetProxy = 5A86C000B00000000 /* PBXContainerItemProxy */; }; + 5AEA8B322F6808270040461A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5AEA8B292F6808270040461A /* EtcdDriverPlugin */; + targetProxy = 5AEA8B312F6808270040461A /* PBXContainerItemProxy */; + }; 5ABCC5AC2F43856700EAF3FC /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5A1091C62EF17EDC0055EA7C /* TablePro */; @@ -2794,6 +2898,55 @@ }; name = Release; }; + 5AEA8B2B2F6808270040461A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D7HJ5TFYCU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Plugins/EtcdDriverPlugin/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSPrincipalClass = "$(PRODUCT_MODULE_NAME).EtcdPlugin"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.TablePro.EtcdDriverPlugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = tableplugin; + }; + name = Debug; + }; + 5AEA8B2C2F6808270040461A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D7HJ5TFYCU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Plugins/EtcdDriverPlugin/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSPrincipalClass = "$(PRODUCT_MODULE_NAME).EtcdPlugin"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.TablePro.EtcdDriverPlugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + WRAPPER_EXTENSION = tableplugin; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -2977,6 +3130,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 5AEA8B2D2F6808270040461A /* Build configuration list for PBXNativeTarget "EtcdDriverPlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5AEA8B2B2F6808270040461A /* Debug */, + 5AEA8B2C2F6808270040461A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 49eddcb0..ecf3b6d2 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -464,6 +464,18 @@ final class PluginManager { return driverPlugins[databaseType.pluginTypeId] } + /// Returns a temporary plugin driver for query building (buildBrowseQuery), or nil + /// if the plugin doesn't implement custom query building (NoSQL hooks). + func queryBuildingDriver(for databaseType: DatabaseType) -> (any PluginDatabaseDriver)? { + guard let plugin = driverPlugin(for: databaseType) else { return nil } + let config = DriverConnectionConfig(host: "", port: 0, username: "", password: "", database: "") + let driver = plugin.createDriver(config: config) + guard driver.buildBrowseQuery(table: "_probe", sortColumns: [], columns: [], limit: 1, offset: 0) != nil else { + return nil + } + return driver + } + func editorLanguage(for databaseType: DatabaseType) -> EditorLanguage { PluginMetadataRegistry.shared.snapshot(forTypeId: databaseType.pluginTypeId)? .editorLanguage ?? .sql diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift index 4d9421b9..a77276f8 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift @@ -372,6 +372,36 @@ extension PluginMetadataRegistry { "Other": ["javascript", "minKey", "maxKey"] ] + let etcdCompletions: [CompletionEntry] = [ + CompletionEntry(label: "get", insertText: "get"), + CompletionEntry(label: "put", insertText: "put"), + CompletionEntry(label: "del", insertText: "del"), + CompletionEntry(label: "watch", insertText: "watch"), + CompletionEntry(label: "lease grant", insertText: "lease grant"), + CompletionEntry(label: "lease revoke", insertText: "lease revoke"), + CompletionEntry(label: "lease timetolive", insertText: "lease timetolive"), + CompletionEntry(label: "lease list", insertText: "lease list"), + CompletionEntry(label: "lease keep-alive", insertText: "lease keep-alive"), + CompletionEntry(label: "member list", insertText: "member list"), + CompletionEntry(label: "endpoint status", insertText: "endpoint status"), + CompletionEntry(label: "endpoint health", insertText: "endpoint health"), + CompletionEntry(label: "compaction", insertText: "compaction"), + CompletionEntry(label: "auth enable", insertText: "auth enable"), + CompletionEntry(label: "auth disable", insertText: "auth disable"), + CompletionEntry(label: "user add", insertText: "user add"), + CompletionEntry(label: "user delete", insertText: "user delete"), + CompletionEntry(label: "user list", insertText: "user list"), + CompletionEntry(label: "role add", insertText: "role add"), + CompletionEntry(label: "role delete", insertText: "role delete"), + CompletionEntry(label: "role list", insertText: "role list"), + CompletionEntry(label: "user grant-role", insertText: "user grant-role"), + CompletionEntry(label: "user revoke-role", insertText: "user revoke-role"), + CompletionEntry(label: "--prefix", insertText: "--prefix"), + CompletionEntry(label: "--limit", insertText: "--limit="), + CompletionEntry(label: "--keys-only", insertText: "--keys-only"), + CompletionEntry(label: "--lease", insertText: "--lease="), + ] + let redisCompletions: [CompletionEntry] = [ CompletionEntry(label: "GET", insertText: "GET"), CompletionEntry(label: "SET", insertText: "SET"), @@ -810,6 +840,84 @@ extension PluginMetadataRegistry { ) ] ) + )), + ("etcd", PluginMetadataSnapshot( + displayName: "etcd", iconName: "etcd-icon", defaultPort: 2_379, + requiresAuthentication: false, supportsForeignKeys: false, supportsSchemaEditing: false, + isDownloadable: true, primaryUrlScheme: "etcd", parameterStyle: .questionMark, + navigationModel: .standard, explainVariants: [], pathFieldRole: .database, + supportsHealthMonitor: true, urlSchemes: ["etcd", "etcds"], postConnectActions: [], + brandColorHex: "#419EDA", + queryLanguageName: "etcdctl", editorLanguage: .bash, + connectionMode: .network, supportsDatabaseSwitching: false, + capabilities: PluginMetadataSnapshot.CapabilityFlags( + supportsSchemaSwitching: false, + supportsImport: false, + supportsExport: true, + supportsSSH: true, + supportsSSL: true, + supportsCascadeDrop: false, + supportsForeignKeyDisable: false, + supportsReadOnlyMode: false, + supportsQueryProgress: false, + requiresReconnectForDatabaseSwitch: false + ), + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "public", + defaultGroupName: "main", + tableEntityName: "Keys", + defaultPrimaryKeyColumn: "Key", + immutableColumns: ["Version", "ModRevision", "CreateRevision"], + systemDatabaseNames: [], + systemSchemaNames: [], + fileExtensions: [], + databaseGroupingStrategy: .flat, + structureColumnFields: [.name, .type, .nullable] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: nil, + statementCompletions: etcdCompletions, + columnTypesByCategory: ["String": ["string"]] + ), + connection: PluginMetadataSnapshot.ConnectionConfig( + additionalConnectionFields: [ + ConnectionField( + id: "etcdKeyPrefix", + label: String(localized: "Key Prefix Root"), + placeholder: "/", + section: .advanced + ), + ConnectionField( + id: "etcdTlsMode", + label: String(localized: "TLS Mode"), + fieldType: .dropdown(options: [ + .init(value: "Disabled", label: "Disabled"), + .init(value: "Required", label: String(localized: "Required (skip verify)")), + .init(value: "VerifyCA", label: String(localized: "Verify CA")), + .init(value: "VerifyIdentity", label: String(localized: "Verify Identity")), + ]), + section: .advanced + ), + ConnectionField( + id: "etcdCaCertPath", + label: String(localized: "CA Certificate"), + placeholder: "/path/to/ca.pem", + section: .advanced + ), + ConnectionField( + id: "etcdClientCertPath", + label: String(localized: "Client Certificate"), + placeholder: "/path/to/client.pem", + section: .advanced + ), + ConnectionField( + id: "etcdClientKeyPath", + label: String(localized: "Client Key"), + placeholder: "/path/to/client-key.pem", + section: .advanced + ), + ] + ) )) ] } diff --git a/TablePro/Core/SSH/LibSSH2Tunnel.swift b/TablePro/Core/SSH/LibSSH2Tunnel.swift index ace02ae2..02679164 100644 --- a/TablePro/Core/SSH/LibSSH2Tunnel.swift +++ b/TablePro/Core/SSH/LibSSH2Tunnel.swift @@ -379,7 +379,7 @@ internal final class LibSSH2Tunnel: @unchecked Sendable { if sent <= 0 { return } totalSent += sent } - } else if readResult == 0 || sessionQueue.sync({ libssh2_channel_eof(channel) }) != 0 { + } else if readResult == 0 || sessionQueue.sync(execute: { libssh2_channel_eof(channel) }) != 0 { return } else if readResult != Int(LIBSSH2_ERROR_EAGAIN) { return diff --git a/TablePro/Core/SSH/LibSSH2TunnelFactory.swift b/TablePro/Core/SSH/LibSSH2TunnelFactory.swift index 85263245..10ab2540 100644 --- a/TablePro/Core/SSH/LibSSH2TunnelFactory.swift +++ b/TablePro/Core/SSH/LibSSH2TunnelFactory.swift @@ -506,7 +506,7 @@ internal enum LibSSH2TunnelFactory { totalSent += sent } } else if channelRead == 0 - || sessionQueue.sync({ libssh2_channel_eof(channel) }) != 0 { + || sessionQueue.sync(execute: { libssh2_channel_eof(channel) }) != 0 { return } else if channelRead != Int(LIBSSH2_ERROR_EAGAIN) { return diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index 0768dee3..0402069f 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -232,6 +232,7 @@ extension DatabaseType { static let duckdb = DatabaseType(rawValue: "DuckDB") static let cassandra = DatabaseType(rawValue: "Cassandra") static let scylladb = DatabaseType(rawValue: "ScyllaDB") + static let etcd = DatabaseType(rawValue: "etcd") } extension DatabaseType: Codable { @@ -251,7 +252,7 @@ extension DatabaseType { static let allKnownTypes: [DatabaseType] = [ .mysql, .mariadb, .postgresql, .sqlite, .redshift, .mongodb, .redis, .mssql, .oracle, .clickhouse, .duckdb, - .cassandra, .scylladb, + .cassandra, .scylladb, .etcd, ] /// Compatibility shim for CaseIterable call sites. @@ -307,6 +308,7 @@ extension DatabaseType { .clickhouse: "ClickHouse", .duckdb: "DuckDB", .cassandra: "Cassandra", .scylladb: "Cassandra", + .etcd: "etcd", ] } diff --git a/TablePro/Models/Query/QueryTab.swift b/TablePro/Models/Query/QueryTab.swift index 87e8723b..170b00ac 100644 --- a/TablePro/Models/Query/QueryTab.swift +++ b/TablePro/Models/Query/QueryTab.swift @@ -440,6 +440,15 @@ struct QueryTab: Identifiable, Equatable { ) -> String { let quote = quoteIdentifier ?? quoteIdentifierFromDialect(PluginManager.shared.sqlDialect(for: databaseType)) let pageSize = AppSettingsManager.shared.dataGrid.defaultPageSize + + // Use plugin's query builder when available (NoSQL drivers like etcd, Redis) + if let pluginDriver = PluginManager.shared.queryBuildingDriver(for: databaseType), + let pluginQuery = pluginDriver.buildBrowseQuery( + table: tableName, sortColumns: [], columns: [], limit: pageSize, offset: 0 + ) { + return pluginQuery + } + switch PluginManager.shared.editorLanguage(for: databaseType) { case .javascript: let escaped = tableName.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 8cd0b0b7..24ee836c 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -1,19643 +1,20007 @@ { - "sourceLanguage": "en", - "strings": { - "": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "" + "sourceLanguage" : "en", + "strings" : { + "" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" } } } }, - "--": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "--" + "--" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "--" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "--" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "--" } } } }, - "—": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "—" + "—" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "—" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "—" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "—" } } } }, - ".%@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": ".%@" + ".%@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : ".%@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": ".%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : ".%@" } } } }, - "·": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "·" + "·" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "·" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "·" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "·" } } } }, - "''": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "''" + "''" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "''" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "''" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "''" } } } }, - "\"%@\" was modified on both this Mac and another device.": {}, - "\"%@\" will be removed from your system. This action cannot be undone.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "\"%@\" sẽ bị xoá khỏi hệ thống. Hành động này không thể hoàn tác." + "\"%@\" was modified on both this Mac and another device." : { + + }, + "\"%@\" will be removed from your system. This action cannot be undone." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"%@\" sẽ bị xoá khỏi hệ thống. Hành động này không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "\"%@\" 将从您的系统中移除。此操作无法撤销。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "\"%@\" 将从您的系统中移除。此操作无法撤销。" } } } }, - "(%@)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "(%@)" + "(%@)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%@)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "(%@)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%@)" } } } }, - "(%lld %@)": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "(%1$lld %2$@)" + "(%lld %@)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "(%1$lld %2$@)" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "(%1$lld %2$@)" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%1$lld %2$@)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "(%lld %@)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld %@)" } } } }, - "(%lld active)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "(%lld đang hoạt động)" + "(%lld active)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld đang hoạt động)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "(%lld 活跃)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld 活跃)" } } } }, - "(%lld hidden)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "(%lld ẩn)" + "(%lld hidden)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld ẩn)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "(%lld 个已隐藏)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld 个已隐藏)" } } } }, - "(%lld)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "(%lld)" + "(%lld)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "(%lld)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%lld)" } } } }, - "(optional)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "(tùy chọn)" + "(optional)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "(tùy chọn)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "(可选)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "(可选)" } } } }, - "/path/to/agent.sock": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "/path/to/agent.sock" + "/path/to/agent.sock" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "/path/to/agent.sock" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "/path/to/agent.sock" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "/path/to/agent.sock" } } } }, - "/path/to/ca-cert.pem": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "/đường/dẫn/tới/ca-cert.pem" + "/path/to/ca-cert.pem" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "/đường/dẫn/tới/ca-cert.pem" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "/path/to/ca-cert.pem" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "/path/to/ca-cert.pem" } } } }, - "/path/to/database.sqlite": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "/đường/dẫn/tới/database.sqlite" + "/path/to/database.sqlite" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "/đường/dẫn/tới/database.sqlite" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "/path/to/database.sqlite" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "/path/to/database.sqlite" } } } }, - "%@ (%lld/%lld)": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@ (%2$lld/%3$lld)" + "%@ (%lld/%lld)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ (%2$lld/%3$lld)" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$@ (%2$lld/%3$lld)" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ (%2$lld/%3$lld)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ (%lld/%lld)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ (%lld/%lld)" } } } }, - "%@ %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@ %2$@" + "%@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %@" } } } }, - "%@ cannot be empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ không được để trống" + "%@ cannot be empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ không được để trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 不能为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 不能为空" } } } }, - "%@ cannot be negative": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ không được là số âm" + "%@ cannot be negative" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ không được là số âm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 不能为负数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 不能为负数" } } } }, - "%@ download": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ lượt tải" + "%@ download" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ lượt tải" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 次下载" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 次下载" } } } }, - "%@ downloads": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ lượt tải" + "%@ downloads" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ lượt tải" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 次下载" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 次下载" } } } }, - "%@ is already assigned to \"%@\". Reassigning will remove it from that action.": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@ is already assigned to \"%2$@\". Reassigning will remove it from that action." + "%@ is already assigned to \"%@\". Reassigning will remove it from that action." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ is already assigned to \"%2$@\". Reassigning will remove it from that action." } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$@ đã được gán cho \"%2$@\". Gán lại sẽ xóa phím tắt khỏi hành động đó." + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ đã được gán cho \"%2$@\". Gán lại sẽ xóa phím tắt khỏi hành động đó." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 已分配给 \"%@\"。重新分配将从该操作中移除它。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 已分配给 \"%@\"。重新分配将从该操作中移除它。" } } } }, - "%@ ms": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ ms" + "%@ ms" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ms" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ ms" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ms" } } } }, - "%@ must be %lld characters or less": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@ must be %2$lld characters or less" + "%@ must be %lld characters or less" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ must be %2$lld characters or less" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$@ phải có %2$lld ký tự trở xuống" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ phải có %2$lld ký tự trở xuống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 必须为 %lld 个字符或更少" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 必须为 %lld 个字符或更少" } } } }, - "%@ must be between %@ and %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@ must be between %2$@ and %3$@" + "%@ must be between %@ and %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ must be between %2$@ and %3$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$@ phải nằm trong khoảng %2$@ đến %3$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ phải nằm trong khoảng %2$@ đến %3$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 必须在 %@ 和 %@ 之间" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 必须在 %@ 和 %@ 之间" } } } }, - "%@ Preview": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước %@" + "%@ Preview" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 预览" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 预览" } } } }, - "%@ requires a Pro license": {}, - "%@ rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ dòng" + "%@ requires a Pro license" : { + + }, + "%@ rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 行" } } } }, - "%@ s": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ giây" + "%@ s" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ giây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 秒" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 秒" } } } }, - "%@ seconds": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ giây" + "%@ seconds" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ giây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 秒" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 秒" } } } }, - "%@, %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@, %2$@" + "%@, %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@, %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@, %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@, %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@, %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@, %@" } } } }, - "%@: %lld": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@: %2$lld" + "%@: %lld" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@: %2$lld" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@: %lld" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@: %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@: %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@: %lld" } } } }, - "%@.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@." + "%@." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@." } } } }, - "%@/%@ rows": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$@/%2$@ rows" + "%@/%@ rows" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@/%2$@ rows" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$@/%2$@ dòng" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@/%2$@ dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@/%@ 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@/%@ 行" } } } }, - "%@ms": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@ms" + "%@ms" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ms" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ms" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ms" } } } }, - "%@s": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%@s" + "%@s" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@s" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@s" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@s" } } } }, - "%lld": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld" + "%lld" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" } } } }, - "%lld bytes": {}, - "%lld in · %lld out": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld in · %2$lld out" + "%lld bytes" : { + + }, + "%lld in · %lld out" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld in · %2$lld out" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld vào · %lld ra" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld vào · %lld ra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 输入 · %lld 输出" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 输入 · %lld 输出" } } } }, - "%lld in / %lld out tokens": { - "extractionState": "stale", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld in / %2$lld out tokens" + "%lld in / %lld out tokens" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld in / %2$lld out tokens" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$lld vào / %2$lld ra token" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld vào / %2$lld ra token" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 输入 / %lld 输出 token" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 输入 / %lld 输出 token" } } } }, - "%lld of %lld": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld of %2$lld" + "%lld of %lld" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld of %2$lld" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$lld / %2$lld" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld / %2$lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld / %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld / %lld" } } } }, - "%lld of %lld rows selected": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld of %2$lld rows selected" + "%lld of %lld rows selected" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld of %2$lld rows selected" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã chọn %1$lld trong %2$lld dòng" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã chọn %1$lld trong %2$lld dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已选择 %lld / %lld 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已选择 %lld / %lld 行" } } } }, - "%lld pt": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld pt" + "%lld pt" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld pt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld pt" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld pt" } } } }, - "%lld row%@ affected": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld row%2$@ affected" + "%lld row%@ affected" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld row%2$@ affected" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld dòng%@ bị ảnh hưởng" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dòng%@ bị ảnh hưởng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 行%@受影响" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 行%@受影响" } } } }, - "%lld seconds": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld giây" + "%lld seconds" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld giây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 秒" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 秒" } } } }, - "%lld skipped (no options)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld bị bỏ qua (không có tùy chọn)" + "%lld skipped (no options)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld bị bỏ qua (không có tùy chọn)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 已跳过(无选项)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 已跳过(无选项)" } } } }, - "%lld statements": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld câu lệnh" + "%lld statements" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld câu lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 条语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 条语句" } } } }, - "%lld statements executed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã thực thi %lld câu lệnh" + "%lld statements executed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã thực thi %lld câu lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已执行 %lld 条语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已执行 %lld 条语句" } } } }, - "%lld table%@ to export": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld table%2$@ to export" + "%lld table%@ to export" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld table%2$@ to export" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld bảng%@ để xuất" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld bảng%@ để xuất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 个表%@待导出" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 个表%@待导出" } } } }, - "%lld tables": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld bảng" + "%lld tables" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld 个表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld 个表" } } } }, - "%lld-%lld of %@ rows": { - "extractionState": "stale", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld-%2$lld of %3$@ rows" + "%lld-%lld of %@ rows" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld-%2$lld of %3$@ rows" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$lld-%2$lld trong %3$@ dòng" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld-%2$lld trong %3$@ dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld-%lld / %@ 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld-%lld / %@ 行" } } } }, - "%lld-%lld of %@%@ rows": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lld-%2$lld of %3$@%4$@ rows" + "%lld-%lld of %@%@ rows" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld-%2$lld of %3$@%4$@ rows" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$lld-%2$lld của %3$@%4$@ dòng" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld-%2$lld của %3$@%4$@ dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld-%lld / %@%@ 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld-%lld / %@%@ 行" } } } }, - "%lld%%": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lld%%" + "%lld%%" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lld%%" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" } } } }, - "%lldm %llds": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "%1$lldm %2$llds" + "%lldm %llds" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lldm %2$llds" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%lldm %llds" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldm %llds" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%lldm %llds" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lldm %llds" } } } }, - "•": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "•" + "•" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "•" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "•" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "•" } } } }, - "••••••••": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "••••••••" + "••••••••" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "••••••••" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "••••••••" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "••••••••" } } } }, - "© 2026 Ngo Quoc Dat.\n%@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "© 2026 Ngo Quoc Dat.\n%@" + "© 2026 Ngo Quoc Dat.\n%@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "© 2026 Ngo Quoc Dat.\n%@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "© 2026 Ngo Quoc Dat.\n%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "© 2026 Ngo Quoc Dat.\n%@" } } } }, - "<1ms": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "<1ms" + "<1ms" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "<1ms" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "<1ms" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "<1ms" } } } }, - "=": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "=" + "=" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "=" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "=" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "=" } } } }, - "~/.pgpass found — matching entry exists": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm thấy ~/.pgpass — có entry khớp" + "~/.pgpass found — matching entry exists" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm thấy ~/.pgpass — có entry khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "~/.pgpass 已找到 - 存在匹配条目" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.pgpass 已找到 - 存在匹配条目" } } } }, - "~/.pgpass found — no matching entry": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm thấy ~/.pgpass — không có entry khớp" + "~/.pgpass found — no matching entry" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm thấy ~/.pgpass — không có entry khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "~/.pgpass 已找到 - 无匹配条目" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.pgpass 已找到 - 无匹配条目" } } } }, - "~/.pgpass has incorrect permissions (needs chmod 0600)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "~/.pgpass có quyền không đúng (cần chmod 0600)" + "~/.pgpass has incorrect permissions (needs chmod 0600)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.pgpass có quyền không đúng (cần chmod 0600)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "~/.pgpass 权限不正确(需要 chmod 0600)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.pgpass 权限不正确(需要 chmod 0600)" } } } }, - "~/.pgpass not found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy ~/.pgpass" + "~/.pgpass not found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy ~/.pgpass" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到 ~/.pgpass" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到 ~/.pgpass" } } } }, - "~/.ssh/id_rsa": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "~/.ssh/id_rsa" + "~/.ssh/id_rsa" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.ssh/id_rsa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "~/.ssh/id_rsa" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/.ssh/id_rsa" } } } }, - "⌘K": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "⌘K" + "⌘K" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "⌘K" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "⌘K" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "⌘K" } } } }, - "⌘T": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "⌘T" + "⌘T" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "⌘T" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "⌘T" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "⌘T" } } } }, - "0": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "0" + "0" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "0" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "0" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "0" } } } }, - "1": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "1" + "1" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "1" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "1" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "1" } } } }, - "1 John Doe john@example.com NULL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "1 John Doe john@example.com NULL" + "1 John Doe john@example.com NULL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 John Doe john@example.com NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "1 John Doe john@example.com NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 John Doe john@example.com NULL" } } } }, - "1 (no batching)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "1 (không gom nhóm)" + "1 (no batching)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 (không gom nhóm)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "1(不分批)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "1(不分批)" } } } }, - "1 of %lld conflicts": {}, - "1 year": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "1 năm" + "1 of %lld conflicts" : { + + }, + "1 year" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 năm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "1 年" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 年" } } } }, - "1,000": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "1,000" + "1,000" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "1,000" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "1,000" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "1,000" } } } }, - "1,000 rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "1.000 dòng" + "1,000 rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "1.000 dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "1,000 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "1,000 行" } } } }, - "2": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "2" + "2" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "2" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "2" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "2" } } } }, - "2 spaces": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "2 dấu cách" + "2 spaces" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "2 dấu cách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "2 个空格" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "2 个空格" } } } }, - "3": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "3" + "3" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "3" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "3" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "3" } } } }, - "4 spaces": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "4 dấu cách" + "4 spaces" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "4 dấu cách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "4 个空格" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "4 个空格" } } } }, - "5,000": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "5,000" + "5,000" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "5,000" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "5,000" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "5,000" } } } }, - "5,000 rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "5.000 dòng" + "5,000 rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "5.000 dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "5,000 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "5,000 行" } } } }, - "6": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "6" + "6" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "6" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "6" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "6" } } } }, - "7 days": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "7 ngày" + "7 days" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "7 ngày" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "7 天" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "7 天" } } } }, - "8": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "8" + "8" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "8" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "8" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "8" } } } }, - "8 spaces": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "8 dấu cách" + "8 spaces" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "8 dấu cách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "8 个空格" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "8 个空格" } } } }, - "10,000": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "10,000" + "10,000" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "10,000" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "10,000" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "10,000" } } } }, - "10,000 rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "10.000 dòng" + "10,000 rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "10.000 dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "10,000 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "10,000 行" } } } }, - "22": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "22" + "22" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "22" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "22" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "22" } } } }, - "30 days": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "30 ngày" + "30 days" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "30 ngày" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "30 天" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "30 天" } } } }, - "30s": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "30 giây" + "30s" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "30 giây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "30秒" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "30秒" } } } }, - "60s": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "60 giây" + "60s" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "60 giây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "60秒" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "60秒" } } } }, - "90 days": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "90 ngày" + "90 days" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "90 ngày" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "90 天" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "90 天" } } } }, - "100": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "100" + "100" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "100" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "100" } } } }, - "100 rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "100 dòng" + "100 rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "100 dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "100 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "100 行" } } } }, - "500": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "500" + "500" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "500" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "500" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "500" } } } }, - "500 rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "500 dòng" + "500 rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "500 dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "500 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "500 行" } } } }, - "A built-in plugin \"%@\" already provides this bundle ID": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin tích hợp \"%@\" đã cung cấp bundle ID này" + "A built-in plugin \"%@\" already provides this bundle ID" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin tích hợp \"%@\" đã cung cấp bundle ID này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "内置插件 \"%@\" 已提供此 bundle ID" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "内置插件 \"%@\" 已提供此 bundle ID" } } } }, - "A fast, lightweight native macOS database client": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ứng dụng quản lý cơ sở dữ liệu gốc macOS nhanh và nhẹ" + "A fast, lightweight native macOS database client" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ứng dụng quản lý cơ sở dữ liệu gốc macOS nhanh và nhẹ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快速、轻量的原生 macOS 数据库客户端" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快速、轻量的原生 macOS 数据库客户端" } } } }, - "A sync conflict was detected and needs to be resolved.": {}, - "About TablePro": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giới thiệu TablePro" + "A sync conflict was detected and needs to be resolved." : { + + }, + "About TablePro" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giới thiệu TablePro" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关于 TablePro" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关于 TablePro" } } } }, - "Accent Color:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Màu nhấn:" + "Accent Color:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Màu nhấn:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "强调色:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "强调色:" } } } }, - "Account:": {}, - "Activate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích hoạt" + "Account:" : { + + }, + "Activate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích hoạt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "激活" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "激活" } } } }, - "Activate License...": {}, - "Activation Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích hoạt thất bại" + "Activate License..." : { + + }, + "Activation Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích hoạt thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "激活失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "激活失败" } } } }, - "Active": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang hoạt động" + "Active" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang hoạt động" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "活跃" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活跃" } } } }, - "Active Connections": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối đang hoạt động" + "Active Connections" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối đang hoạt động" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "活跃连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活跃连接" } } } }, - "ACTIVE CONNECTIONS": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "KẾT NỐI ĐANG HOẠT ĐỘNG" + "ACTIVE CONNECTIONS" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "KẾT NỐI ĐANG HOẠT ĐỘNG" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "活跃连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活跃连接" } } } }, - "Add": {}, - "Add Check Constraint": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm ràng buộc kiểm tra" + "Add" : { + + }, + "Add Check Constraint" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm ràng buộc kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加检查约束" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加检查约束" } } } }, - "Add Column": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm cột" + "Add Column" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加列" } } } }, - "Add columns first": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm cột trước" + "Add columns first" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm cột trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "请先添加列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请先添加列" } } } }, - "Add filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm bộ lọc" + "Add filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加筛选" } } } }, - "Add Filter (Cmd+Shift+F)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm bộ lọc (Cmd+Shift+F)" + "Add Filter (Cmd+Shift+F)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm bộ lọc (Cmd+Shift+F)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加筛选 (Cmd+Shift+F)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加筛选 (Cmd+Shift+F)" } } } }, - "Add Foreign Key": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm khóa ngoại" + "Add Foreign Key" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm khóa ngoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加外键" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加外键" } } } }, - "Add Index": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm chỉ mục" + "Add Index" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm chỉ mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加索引" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加索引" } } } }, - "Add indexes to improve query performance on frequently searched columns": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm chỉ mục để cải thiện hiệu suất truy vấn trên các cột thường xuyên tìm kiếm" + "Add indexes to improve query performance on frequently searched columns" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm chỉ mục để cải thiện hiệu suất truy vấn trên các cột thường xuyên tìm kiếm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加索引以提升常用搜索列的查询性能" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加索引以提升常用搜索列的查询性能" } } } }, - "Add Jump Host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm Jump Host" + "Add Jump Host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm Jump Host" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加跳板机" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加跳板机" } } } }, - "Add Provider": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm nhà cung cấp" + "Add Provider" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm nhà cung cấp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加提供商" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加提供商" } } } }, - "Add Row": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm dòng" + "Add Row" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加行" } } } }, - "Add validation rules to ensure data integrity": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thêm quy tắc xác thực để đảm bảo tính toàn vẹn dữ liệu" + "Add validation rules to ensure data integrity" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thêm quy tắc xác thực để đảm bảo tính toàn vẹn dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "添加验证规则以确保数据完整性" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加验证规则以确保数据完整性" } } } }, - "admin": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "admin" + "admin" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "admin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "admin" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "admin" } } } }, - "Agent Socket": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Agent Socket" + "Agent Socket" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent Socket" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Agent Socket" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent Socket" } } } }, - "AI": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "AI" + "AI" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI" } } } }, - "AI Chat": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "AI Chat" + "AI Chat" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI Chat" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI 对话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 对话" } } } }, - "AI is disabled for this connection.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "AI bị tắt cho kết nối này." + "AI is disabled for this connection." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI bị tắt cho kết nối này." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此连接已禁用 AI。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此连接已禁用 AI。" } } } }, - "AI Policy": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chính sách AI" + "AI Policy" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chính sách AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI 策略" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 策略" } } } }, - "AI Provider": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhà cung cấp AI" + "AI Provider" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhà cung cấp AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI 提供商" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 提供商" } } } }, - "AI-powered SQL completions appear as ghost text while typing. Press Tab to accept, Escape to dismiss.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gợi ý SQL bằng AI xuất hiện dưới dạng văn bản mờ khi gõ. Nhấn Tab để chấp nhận, Escape để bỏ qua." + "AI-powered SQL completions appear as ghost text while typing. Press Tab to accept, Escape to dismiss." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gợi ý SQL bằng AI xuất hiện dưới dạng văn bản mờ khi gõ. Nhấn Tab để chấp nhận, Escape để bỏ qua." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI 驱动的 SQL 补全在输入时以虚影文本显示。按 Tab 接受,按 Escape 关闭。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 驱动的 SQL 补全在输入时以虚影文本显示。按 Tab 接受,按 Escape 关闭。" } } } }, - "Alert": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cảnh báo" + "Alert" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cảnh báo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "警告" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "警告" } } } }, - "Alert (Full)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cảnh báo (Đầy đủ)" + "Alert (Full)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cảnh báo (Đầy đủ)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "警告(完整)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "警告(完整)" } } } }, - "Algorithm": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thuật toán" + "Algorithm" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thuật toán" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "算法" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "算法" } } } }, - "All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tất cả" + "All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "全部" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部" } } } }, - "All %lld rows selected": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã chọn tất cả %lld dòng" + "All %lld rows selected" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã chọn tất cả %lld dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已选择全部 %lld 行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已选择全部 %lld 行" } } } }, - "All columns": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tất cả cột" + "All columns" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tất cả cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所有列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有列" } } } }, - "ALL DATABASES": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TẤT CẢ CƠ SỞ DỮ LIỆU" + "ALL DATABASES" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TẤT CẢ CƠ SỞ DỮ LIỆU" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所有数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有数据库" } } } }, - "All rights reserved.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã đăng ký bản quyền." + "All rights reserved." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã đăng ký bản quyền." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保留所有权利。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留所有权利。" } } } }, - "ALL SCHEMAS": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TẤT CẢ SCHEMA" + "ALL SCHEMAS" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TẤT CẢ SCHEMA" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所有 SCHEMA" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有 SCHEMA" } } } }, - "All Time": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tất cả" + "All Time" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所有时间" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有时间" } } } }, - "Allow": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cho phép" + "Allow" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cho phép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "允许" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "允许" } } } }, - "Allow AI Access": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cho phép truy cập AI" + "Allow AI Access" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cho phép truy cập AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "允许 AI 访问" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "允许 AI 访问" } } } }, - "Also handles": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cũng xử lý" + "Also handles" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cũng xử lý" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "兼容类型" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "兼容类型" } } } }, - "Also handles:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cũng hỗ trợ:" + "Also handles:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cũng hỗ trợ:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "还支持:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "还支持:" } } } }, - "Alternate Row": {}, - "Always": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Luôn luôn" + "Alternate Row" : { + + }, + "Always" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luôn luôn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "始终" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终" } } } }, - "Always Allow": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Luôn cho phép" + "Always Allow" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luôn cho phép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "始终允许" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终允许" } } } }, - "An unknown sync error occurred: %@": {}, - "and": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "và" + "An unknown sync error occurred: %@" : { + + }, + "and" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "và" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "和" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "和" } } } }, - "AND": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "AND" + "AND" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "AND" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AND" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AND" } } } }, - "Animations": {}, - "API Key": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "API Key" + "Animations" : { + + }, + "API Key" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "API Key" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" } } } }, - "Appearance": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giao diện" + "Appearance" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giao diện" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "外观" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "外观" } } } }, - "Appearance:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giao diện:" + "Appearance:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giao diện:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "外观:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "外观:" } } } }, - "Apply All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Áp dụng tất cả" + "Apply All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áp dụng tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "全部应用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部应用" } } } }, - "Apply Changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Áp dụng thay đổi" + "Apply Changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áp dụng thay đổi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "应用更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用更改" } } } }, - "Apply this filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Áp dụng bộ lọc này" + "Apply this filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áp dụng bộ lọc này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "应用此筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用此筛选" } } } }, - "Apply This Filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Áp dụng bộ lọc này" + "Apply This Filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áp dụng bộ lọc này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "应用此筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用此筛选" } } } }, - "Applying or clearing filters will reload data and discard all unsaved changes.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Áp dụng hoặc xóa bộ lọc sẽ tải lại dữ liệu và hủy tất cả thay đổi chưa lưu." + "Applying or clearing filters will reload data and discard all unsaved changes." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Áp dụng hoặc xóa bộ lọc sẽ tải lại dữ liệu và hủy tất cả thay đổi chưa lưu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "应用或清除筛选条件将重新加载数据并丢弃所有未保存的更改。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用或清除筛选条件将重新加载数据并丢弃所有未保存的更改。" } } } }, - "Are you sure you want to delete \"%@\"?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có chắc muốn xóa \"%@\" không?" + "Are you sure you want to delete \"%@\"?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có chắc muốn xóa \"%@\" không?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "确定要删除 \"%@\" 吗?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定要删除 \"%@\" 吗?" } } } }, - "Are you sure you want to delete this connection? This cannot be undone.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có chắc muốn xóa kết nối này? Thao tác này không thể hoàn tác." + "Are you sure you want to delete this connection? This cannot be undone." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có chắc muốn xóa kết nối này? Thao tác này không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "确定要删除此连接吗?此操作无法撤消。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定要删除此连接吗?此操作无法撤消。" } } } }, - "Are you sure you want to disconnect from this database?": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có chắc muốn ngắt kết nối khỏi cơ sở dữ liệu này không?" + "Are you sure you want to disconnect from this database?" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có chắc muốn ngắt kết nối khỏi cơ sở dữ liệu này không?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "确定要断开与此数据库的连接吗?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定要断开与此数据库的连接吗?" } } } }, - "Are you sure you want to execute this query?\n\n%@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có chắc chắn muốn thực thi truy vấn này?\n\n%@" + "Are you sure you want to execute this query?\n\n%@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có chắc chắn muốn thực thi truy vấn này?\n\n%@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "确定要执行此查询吗?\n\n%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定要执行此查询吗?\n\n%@" } } } }, - "Ask about your database...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hỏi về cơ sở dữ liệu của bạn..." + "Ask about your database..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hỏi về cơ sở dữ liệu của bạn..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "询问您的数据库..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "询问您的数据库..." } } } }, - "Ask AI about your database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hỏi AI về cơ sở dữ liệu của bạn" + "Ask AI about your database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hỏi AI về cơ sở dữ liệu của bạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "向 AI 询问您的数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "向 AI 询问您的数据库" } } } }, - "Ask AI to Fix": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhờ AI sửa lỗi" + "Ask AI to Fix" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhờ AI sửa lỗi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "让 AI 修复" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "让 AI 修复" } } } }, - "Ask Each Time": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hỏi mỗi lần" + "Ask Each Time" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hỏi mỗi lần" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每次询问" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每次询问" } } } }, - "Auth": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực" + "Auth" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "认证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "认证" } } } }, - "Authenticate to execute database operations": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực để thực thi thao tác cơ sở dữ liệu" + "Authenticate to execute database operations" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực để thực thi thao tác cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "验证身份以执行数据库操作" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证身份以执行数据库操作" } } } }, - "Authentication": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực" + "Authentication" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "身份验证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "身份验证" } } } }, - "Authentication failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực thất bại: %@" + "Authentication failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "身份验证失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "身份验证失败:%@" } } } }, - "Authentication failed. Check your API key.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực thất bại. Kiểm tra API key của bạn." + "Authentication failed. Check your API key." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực thất bại. Kiểm tra API key của bạn." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "身份验证失败。请检查您的 API key。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "身份验证失败。请检查您的 API key。" } } } }, - "Authentication required to execute operations": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cần xác thực để thực thi thao tác" + "Authentication required to execute operations" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cần xác thực để thực thi thao tác" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "执行操作需要身份验证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "执行操作需要身份验证" } } } }, - "Authentication required to execute write operations": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cần xác thực để thực thi thao tác ghi" + "Authentication required to execute write operations" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cần xác thực để thực thi thao tác ghi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "执行写入操作需要身份验证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "执行写入操作需要身份验证" } } } }, - "Author": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tác giả" + "Author" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tác giả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "作者" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "作者" } } } }, - "Auto": {}, - "AUTO": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TỰ ĐỘNG" + "Auto" : { + + }, + "AUTO" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TỰ ĐỘNG" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动" } } } }, - "Auto cleanup on startup": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động dọn dẹp khi khởi động" + "Auto cleanup on startup" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động dọn dẹp khi khởi động" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启动时自动清理" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启动时自动清理" } } } }, - "Auto Generate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động tạo" + "Auto Generate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động tạo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动生成" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动生成" } } } }, - "Auto Inc": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự tăng" + "Auto Inc" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự tăng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自增" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自增" } } } }, - "Auto Increment": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động tăng" + "Auto Increment" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động tăng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动递增" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动递增" } } } }, - "Auto-indent": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động thụt lề" + "Auto-indent" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động thụt lề" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动缩进" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动缩进" } } } }, - "Auto-show inspector on row select": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động hiện thanh kiểm tra khi chọn dòng" + "Auto-show inspector on row select" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động hiện thanh kiểm tra khi chọn dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选中行时自动显示检查器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选中行时自动显示检查器" } } } }, - "Automatically check for updates": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động kiểm tra cập nhật" + "Automatically check for updates" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động kiểm tra cập nhật" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动检查更新" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动检查更新" } } } }, - "Avg Row": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TB dòng" + "Avg Row" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TB dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "平均行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "平均行" } } } }, - "Background": {}, - "Badge Background": {}, - "Badges": {}, - "Base32-encoded secret from your authenticator setup": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mã bí mật mã hóa Base32 từ thiết lập xác thực của bạn" + "Background" : { + + }, + "Badge Background" : { + + }, + "Badges" : { + + }, + "Base32-encoded secret from your authenticator setup" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mã bí mật mã hóa Base32 từ thiết lập xác thực của bạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "来自身份验证器设置的 Base32 编码密钥" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "来自身份验证器设置的 Base32 编码密钥" } } } }, - "bastion.example.com": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "bastion.example.com" + "bastion.example.com" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "bastion.example.com" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "bastion.example.com" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "bastion.example.com" } } } }, - "between": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "giữa" + "between" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "giữa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "介于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "介于" } } } }, - "Blue": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xanh dương" + "Blue" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xanh dương" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "蓝色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "蓝色" } } } }, - "Body": {}, - "Bool False": {}, - "Bool True": {}, - "Border": {}, - "Browse": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Duyệt" + "Body" : { + + }, + "Bool False" : { + + }, + "Bool True" : { + + }, + "Border" : { + + }, + "Browse" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duyệt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "浏览" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "浏览" } } } }, - "Browse...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Duyệt..." + "Browse..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duyệt..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "浏览..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "浏览..." } } } }, - "Built-in": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tích hợp sẵn" + "Built-in" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tích hợp sẵn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "内置" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "内置" } } } }, - "Built-in plugins cannot be uninstalled": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể gỡ cài đặt plugin tích hợp sẵn" + "Built-in plugins cannot be uninstalled" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể gỡ cài đặt plugin tích hợp sẵn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法卸载内置插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法卸载内置插件" } } } }, - "Bundle ID": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bundle ID" + "Bundle ID" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bundle ID" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Bundle ID" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bundle ID" } } } }, - "Bundle ID:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bundle ID:" + "Bundle ID:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bundle ID:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Bundle ID:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bundle ID:" } } } }, - "CA Cert": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chứng chỉ CA" + "CA Cert" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chứng chỉ CA" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "CA 证书" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CA 证书" } } } }, - "CA Certificate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chứng chỉ CA" + "CA Certificate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chứng chỉ CA" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "CA 证书" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CA 证书" } } } }, - "Cancel": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy" + "Cancel" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "取消" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" } } } }, - "Cannot execute write queries: connection is read-only": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể thực thi truy vấn ghi: kết nối chỉ đọc" + "Cannot execute write queries: connection is read-only" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể thực thi truy vấn ghi: kết nối chỉ đọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法执行写入查询:连接为只读" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法执行写入查询:连接为只读" } } } }, - "Cannot format empty SQL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể định dạng SQL trống" + "Cannot format empty SQL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể định dạng SQL trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法格式化空 SQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法格式化空 SQL" } } } }, - "Capabilities": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khả năng" + "Capabilities" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khả năng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "功能" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "功能" } } } }, - "Capabilities:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tính năng:" + "Capabilities:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tính năng:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "功能:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "功能:" } } } }, - "Caption": {}, - "Card Background": {}, - "Cascade": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cascade" + "Caption" : { + + }, + "Card Background" : { + + }, + "Cascade" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cascade" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Cascade" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cascade" } } } }, - "Category": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Danh mục" + "Category" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Danh mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分类" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分类" } } } }, - "Cell Renderer": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trình hiển thị ô" + "Cell Renderer" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trình hiển thị ô" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "单元格渲染器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "单元格渲染器" } } } }, - "Cell Value": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị ô" + "Cell Value" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị ô" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "单元格值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "单元格值" } } } }, - "Change Color": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đổi màu" + "Change Color" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đổi màu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "更改颜色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改颜色" } } } }, - "Change File": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đổi tệp" + "Change File" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đổi tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "更改文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改文件" } } } }, - "Change File...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đổi tệp..." + "Change File..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đổi tệp..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "更改文件..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改文件..." } } } }, - "Character Set": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bộ ký tự" + "Character Set" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bộ ký tự" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "字符集" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字符集" } } } }, - "Charset (e.g., utf8mb4)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bộ ký tự (vd: utf8mb4)" + "Charset (e.g., utf8mb4)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bộ ký tự (vd: utf8mb4)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "字符集(如 utf8mb4)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字符集(如 utf8mb4)" } } } }, - "Chat": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chat" + "Chat" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chat" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "对话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "对话" } } } }, - "Check for Updates...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểm tra cập nhật..." + "Check for Updates..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra cập nhật..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "检查更新..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查更新..." } } } }, - "Choose a query from the list\nto see its full content here.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn một truy vấn từ danh sách\nđể xem nội dung đầy đủ tại đây." + "Choose a query from the list\nto see its full content here." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn một truy vấn từ danh sách\nđể xem nội dung đầy đủ tại đây." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从列表中选择一个查询\n以在此处查看其完整内容。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从列表中选择一个查询\n以在此处查看其完整内容。" } } } }, - "Clear": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa" + "Clear" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除" } } } }, - "Clear All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa tất cả" + "Clear All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "全部清除" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部清除" } } } }, - "Clear all history": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xoá toàn bộ lịch sử" + "Clear all history" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xoá toàn bộ lịch sử" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除全部历史" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除全部历史" } } } }, - "Clear All History?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa toàn bộ lịch sử?" + "Clear All History?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa toàn bộ lịch sử?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除全部历史?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除全部历史?" } } } }, - "Clear all query history": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa toàn bộ lịch sử truy vấn" + "Clear all query history" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa toàn bộ lịch sử truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除全部查询历史" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除全部查询历史" } } } }, - "Clear Conversation": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa hội thoại" + "Clear Conversation" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa hội thoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除对话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除对话" } } } }, - "Clear History...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa lịch sử..." + "Clear History..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa lịch sử..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除历史..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除历史..." } } } }, - "Clear Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xoá truy vấn" + "Clear Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xoá truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除查询" } } } }, - "Clear Query (⌘+Delete)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa truy vấn (⌘+Delete)" + "Clear Query (⌘+Delete)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa truy vấn (⌘+Delete)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除查询 (⌘+Delete)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除查询 (⌘+Delete)" } } } }, - "Clear Recents": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xoá gần đây" + "Clear Recents" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xoá gần đây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除最近记录" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除最近记录" } } } }, - "Clear search": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa tìm kiếm" + "Clear search" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa tìm kiếm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除搜索" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除搜索" } } } }, - "Clear Search": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa tìm kiếm" + "Clear Search" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa tìm kiếm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除搜索" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除搜索" } } } }, - "Clear Selection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bỏ chọn" + "Clear Selection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bỏ chọn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "取消选择" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消选择" } } } }, - "Clear table filter": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa bộ lọc bảng" + "Clear table filter" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa bộ lọc bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清除表筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除表筛选" } } } }, - "Click + to add a relationship between this table and another": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấn + để thêm mối quan hệ giữa bảng này và bảng khác" + "Click + to add a relationship between this table and another" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấn + để thêm mối quan hệ giữa bảng này và bảng khác" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "点击 + 添加此表与其他表之间的关系" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击 + 添加此表与其他表之间的关系" } } } }, - "Click + to create your first connection": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấn + để tạo kết nối đầu tiên" + "Click + to create your first connection" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấn + để tạo kết nối đầu tiên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "点击 + 创建您的第一个连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击 + 创建您的第一个连接" } } } }, - "Click a table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấn vào một bảng" + "Click a table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấn vào một bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "点击一个表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击一个表" } } } }, - "Click to load models": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấp để tải danh sách model" + "Click to load models" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấp để tải danh sách model" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "点击加载模型" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击加载模型" } } } }, - "Click to show all tables with metadata": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấn để hiện tất cả bảng với siêu dữ liệu" + "Click to show all tables with metadata" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấn để hiện tất cả bảng với siêu dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "点击显示所有表及其元数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击显示所有表及其元数据" } } } }, - "Client Cert": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chứng chỉ máy khách" + "Client Cert" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chứng chỉ máy khách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "客户端证书" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "客户端证书" } } } }, - "Client Certificates (Optional)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chứng chỉ máy khách (Tùy chọn)" + "Client Certificate" : { + + }, + "Client Certificates (Optional)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chứng chỉ máy khách (Tùy chọn)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "客户端证书(可选)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "客户端证书(可选)" } } } }, - "Client Key": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khóa máy khách" + "Client Key" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khóa máy khách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "客户端密钥" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "客户端密钥" } } } }, - "Clipboard is empty or contains no text data.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bộ nhớ tạm trống hoặc không chứa dữ liệu văn bản." + "Clipboard is empty or contains no text data." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bộ nhớ tạm trống hoặc không chứa dữ liệu văn bản." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "剪贴板为空或不包含文本数据。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "剪贴板为空或不包含文本数据。" } } } }, - "Close": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đóng" + "Close" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đóng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" } } } }, - "Close (ESC)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đóng (ESC)" + "Close (ESC)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đóng (ESC)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭 (ESC)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭 (ESC)" } } } }, - "Close Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đóng tab" + "Close Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đóng tab" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭标签页" } } } }, - "Closing this tab will discard all unsaved changes.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đóng tab này sẽ hủy tất cả thay đổi chưa lưu." + "Closing this tab will discard all unsaved changes." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đóng tab này sẽ hủy tất cả thay đổi chưa lưu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭此标签页将丢弃所有未保存的更改。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭此标签页将丢弃所有未保存的更改。" } } } }, - "CMD": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CMD" + "CMD" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CMD" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "CMD" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CMD" } } } }, - "Collation": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đối chiếu" + "Collation" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đối chiếu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "排序规则" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "排序规则" } } } }, - "Color": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Màu sắc" + "Color" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Màu sắc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "颜色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "颜色" } } } }, - "Color %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Màu %@" + "Color %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Màu %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "颜色 %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "颜色 %@" } } } }, - "Colors": {}, - "Column": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột" + "Colors" : { + + }, + "Column" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列" } } } }, - "Column count mismatch on line %lld: expected %lld columns, found %lld.": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Column count mismatch on line %1$lld: expected %2$lld columns, found %3$lld." + "Column count mismatch on line %lld: expected %lld columns, found %lld." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Column count mismatch on line %1$lld: expected %2$lld columns, found %3$lld." } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số cột không khớp ở dòng %1$lld: mong đợi %2$lld cột, tìm thấy %3$lld." + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số cột không khớp ở dòng %1$lld: mong đợi %2$lld cột, tìm thấy %3$lld." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "第 %lld 行列数不匹配:期望 %lld 列,实际 %lld 列。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第 %lld 行列数不匹配:期望 %lld 列,实际 %lld 列。" } } } }, - "Column Details": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chi tiết cột" + "Column Details" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chi tiết cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列详情" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列详情" } } } }, - "Column name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên cột" + "Column name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列名" } } } }, - "Column Name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên cột" + "Column Name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列名" } } } }, - "Column: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột: %@" + "Column: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列:%@" } } } }, - "Columns": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột" + "Columns" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列" } } } }, - "Columns (comma-separated)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Các cột (phân tách bằng dấu phẩy)" + "Columns (comma-separated)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Các cột (phân tách bằng dấu phẩy)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "列(逗号分隔)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列(逗号分隔)" } } } }, - "Comfortable": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thoải mái" + "Comfortable" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thoải mái" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "舒适" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "舒适" } } } }, - "Command Preview": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước lệnh" + "Command Preview" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "命令预览" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "命令预览" } } } }, - "Comment": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ghi chú" + "Comment" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ghi chú" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "注释" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "注释" } } } }, - "Compact": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thu gọn" + "Compact" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thu gọn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "紧凑" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "紧凑" } } } }, - "Compress the file using Gzip": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nén tệp bằng Gzip" + "Compress the file using Gzip" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nén tệp bằng Gzip" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "使用 Gzip 压缩文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 Gzip 压缩文件" } } } }, - "Config Host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Máy chủ cấu hình" + "Config Host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máy chủ cấu hình" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "配置主机" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置主机" } } } }, - "Configure an AI provider in Settings to start chatting.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cấu hình nhà cung cấp AI trong Cài đặt để bắt đầu trò chuyện." + "Configure an AI provider in Settings to start chatting." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cấu hình nhà cung cấp AI trong Cài đặt để bắt đầu trò chuyện." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在设置中配置 AI 提供商以开始对话。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在设置中配置 AI 提供商以开始对话。" } } } }, - "Connect": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối" + "Connect" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接" } } } }, - "Connect Anyway": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Vẫn kết nối" + "Connect Anyway" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vẫn kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "仍然连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仍然连接" } } } }, - "Connected": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã kết nối" + "Connected" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已连接" } } } }, - "Connecting": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang kết nối" + "Connecting" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接中" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接中" } } } }, - "Connecting...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang kết nối..." + "Connecting..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang kết nối..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接中..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接中..." } } } }, - "Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối" + "Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接" } } } }, - "Connection Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối thất bại" + "Connection Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接失败" } } } }, - "Connection lost": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mất kết nối" + "Connection lost" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mất kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接丢失" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接丢失" } } } }, - "Connection name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên kết nối" + "Connection name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接名称" } } } }, - "Connection Not Found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy kết nối" + "Connection Not Found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到连接" } } } }, - "Connection Status": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trạng thái kết nối" + "Connection Status" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trạng thái kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接状态" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接状态" } } } }, - "Connection successful": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối thành công" + "Connection successful" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối thành công" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接成功" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接成功" } } } }, - "Connection Switcher": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển đổi kết nối" + "Connection Switcher" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển đổi kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换连接" } } } }, - "Connection test failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểm tra kết nối thất bại" + "Connection test failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra kết nối thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接测试失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接测试失败" } } } }, - "Connection Test Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểm tra kết nối thất bại" + "Connection Test Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra kết nối thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接测试失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接测试失败" } } } }, - "Connection URL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "URL kết nối" + "Connection URL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接 URL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接 URL" } } } }, - "Connection URL cannot be empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "URL kết nối không được để trống" + "Connection URL cannot be empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL kết nối không được để trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接 URL 不能为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接 URL 不能为空" } } } }, - "Connection URL must include a host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "URL kết nối phải bao gồm host" + "Connection URL must include a host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL kết nối phải bao gồm host" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接 URL 必须包含主机" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接 URL 必须包含主机" } } } }, - "Connections:": {}, - "Constraint name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên ràng buộc" + "Connections:" : { + + }, + "Constraint name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên ràng buộc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "约束名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "约束名称" } } } }, - "contains": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "chứa" + "contains" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "chứa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含" } } } }, - "Context": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ngữ cảnh" + "Context" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngữ cảnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上下文" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上下文" } } } }, - "Continue": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tiếp tục" + "Continue" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiếp tục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "继续" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "继续" } } } }, - "Control Background": {}, - "Conversation History": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lịch sử hội thoại" + "Control Background" : { + + }, + "Conversation History" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lịch sử hội thoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "对话历史" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "对话历史" } } } }, - "Convert line break to space": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển xuống dòng thành dấu cách" + "Convert line break to space" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển xuống dòng thành dấu cách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将换行转换为空格" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将换行转换为空格" } } } }, - "Convert NULL to empty": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển NULL thành rỗng" + "Convert NULL to empty" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển NULL thành rỗng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将 NULL 转换为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将 NULL 转换为空" } } } }, - "Convert NULL to EMPTY": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển NULL thành RỖNG" + "Convert NULL to EMPTY" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển NULL thành RỖNG" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将 NULL 转换为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将 NULL 转换为空" } } } }, - "Copied": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã sao chép" + "Copied" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã sao chép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已复制" } } } }, - "Copied!": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã sao chép!" + "Copied!" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã sao chép!" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已复制!" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已复制!" } } } }, - "Copy": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép" + "Copy" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" } } } }, - "Copy All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép tất cả" + "Copy All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "全部复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部复制" } } } }, - "Copy as": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép dạng" + "Copy as" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép dạng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制为" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制为" } } } }, - "Copy as Hex": {}, - "Copy as JSON": {}, - "Copy as URL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép dạng URL" + "Copy as Hex" : { + + }, + "Copy as JSON" : { + + }, + "Copy as URL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép dạng URL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制为 URL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制为 URL" } } } }, - "Copy Column Name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép tên cột" + "Copy Column Name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép tên cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制列名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制列名" } } } }, - "Copy Name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép tên" + "Copy Name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép tên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制名称" } } } }, - "Copy Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép truy vấn" + "Copy Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制查询" } } } }, - "Copy SQL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép SQL" + "Copy SQL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制 SQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制 SQL" } } } }, - "Copy this statement to clipboard": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép câu lệnh này vào bộ nhớ tạm" + "Copy this statement to clipboard" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép câu lệnh này vào bộ nhớ tạm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将此语句复制到剪贴板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将此语句复制到剪贴板" } } } }, - "Copy Value": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép giá trị" + "Copy Value" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép giá trị" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制值" } } } }, - "Copy with Headers": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sao chép kèm tiêu đề" + "Copy with Headers" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sao chép kèm tiêu đề" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制(含表头)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制(含表头)" } } } }, - "Corner Radius": {}, - "Could not fetch plugin registry": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể tải danh sách plugin" + "Corner Radius" : { + + }, + "Could not fetch plugin registry" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể tải danh sách plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法获取插件注册表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法获取插件注册表" } } } }, - "Count all rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đếm tất cả hàng" + "Count all rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đếm tất cả hàng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "统计全部行数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "统计全部行数" } } } }, - "Counting...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang đếm..." + "Counting..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang đếm..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "统计中..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "统计中..." } } } }, - "Create": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo" + "Create" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建" } } } }, - "Create a connection to get started": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo kết nối để bắt đầu" + "Create a connection to get started" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo kết nối để bắt đầu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建连接以开始使用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建连接以开始使用" } } } }, - "Create a connection to get started with\nyour databases.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo kết nối để bắt đầu sử dụng\ncơ sở dữ liệu của bạn." + "Create a connection to get started with\nyour databases." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo kết nối để bắt đầu sử dụng\ncơ sở dữ liệu của bạn." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建连接以开始使用\n您的数据库。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建连接以开始使用\n您的数据库。" } } } }, - "Create connection...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo kết nối..." + "Create connection..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo kết nối..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建连接..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建连接..." } } } }, - "Create Database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo cơ sở dữ liệu" + "Create Database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建数据库" } } } }, - "Create new database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo cơ sở dữ liệu mới" + "Create new database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo cơ sở dữ liệu mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建新数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新数据库" } } } }, - "Create New Group": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo nhóm mới" + "Create New Group" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo nhóm mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建新分组" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新分组" } } } }, - "Create New Group...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo nhóm mới..." + "Create New Group..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo nhóm mới..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建新分组..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新分组..." } } } }, - "Create New Tag": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo thẻ mới" + "Create New Tag" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo thẻ mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建新标签" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新标签" } } } }, - "Create New Tag...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo thẻ mới..." + "Create New Tag..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo thẻ mới..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建新标签..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新标签..." } } } }, - "Create New View...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo view mới..." + "Create New View..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo view mới..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建新视图..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新视图..." } } } }, - "Created": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã tạo" + "Created" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã tạo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已创建" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已创建" } } } }, - "Creating...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang tạo..." + "Creating..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang tạo..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建中..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建中..." } } } }, - "CRITICAL: Transaction rollback failed - database may be in inconsistent state: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "NGHIÊM TRỌNG: Hoàn tác giao dịch thất bại - cơ sở dữ liệu có thể ở trạng thái không nhất quán: %@" + "CRITICAL: Transaction rollback failed - database may be in inconsistent state: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "NGHIÊM TRỌNG: Hoàn tác giao dịch thất bại - cơ sở dữ liệu có thể ở trạng thái không nhất quán: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "严重:事务回滚失败 - 数据库可能处于不一致状态:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "严重:事务回滚失败 - 数据库可能处于不一致状态:%@" } } } }, - "CURDATE()": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CURDATE()" + "CURDATE()" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CURDATE()" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "CURDATE()" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CURDATE()" } } } }, - "current": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "hiện tại" + "current" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "hiện tại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "当前" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前" } } } }, - "Current database: %@ (⌘K to switch)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu hiện tại: %@ (⌘K để chuyển)" + "Current database: %@ (⌘K to switch)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu hiện tại: %@ (⌘K để chuyển)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "当前数据库:%@(⌘K 切换)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前数据库:%@(⌘K 切换)" } } } }, - "Current database: %@ (read-only, ⌘K to switch)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu hiện tại: %@ (chỉ đọc, ⌘K để chuyển)" + "Current database: %@ (read-only, ⌘K to switch)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu hiện tại: %@ (chỉ đọc, ⌘K để chuyển)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "当前数据库:%@(只读,⌘K 切换)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前数据库:%@(只读,⌘K 切换)" } } } }, - "Current Line": {}, - "Current schema: %@ (⌘K to switch)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Schema hiện tại: %@ (⌘K để chuyển)" + "Current Line" : { + + }, + "Current schema: %@ (⌘K to switch)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema hiện tại: %@ (⌘K để chuyển)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "当前 schema:%@(⌘K 切换)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前 schema:%@(⌘K 切换)" } } } }, - "Current schema: %@ (read-only, ⌘K to switch)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Schema hiện tại: %@ (chỉ đọc, ⌘K để chuyển)" + "Current schema: %@ (read-only, ⌘K to switch)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema hiện tại: %@ (chỉ đọc, ⌘K để chuyển)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "当前 schema:%@(只读,⌘K 切换)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前 schema:%@(只读,⌘K 切换)" } } } }, - "CURRENT_TIMESTAMP()": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CURRENT_TIMESTAMP()" + "CURRENT_TIMESTAMP()" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CURRENT_TIMESTAMP()" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "CURRENT_TIMESTAMP()" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CURRENT_TIMESTAMP()" } } } }, - "Cursor": {}, - "Cursor position %lld exceeds SQL length (%lld)": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Cursor position %1$lld exceeds SQL length (%2$lld)" + "Cursor" : { + + }, + "Cursor position %lld exceeds SQL length (%lld)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cursor position %1$lld exceeds SQL length (%2$lld)" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Vị trí con trỏ %1$lld vượt quá độ dài SQL (%2$lld)" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vị trí con trỏ %1$lld vượt quá độ dài SQL (%2$lld)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "光标位置 %lld 超出 SQL 长度(%lld)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "光标位置 %lld 超出 SQL 长度(%lld)" } } } }, - "CURTIME()": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CURTIME()" + "CURTIME()" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CURTIME()" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "CURTIME()" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CURTIME()" } } } }, - "Custom": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tùy chỉnh" + "Custom" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tùy chỉnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自定义" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自定义" } } } }, - "Custom Path": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đường dẫn tùy chỉnh" + "Custom Path" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đường dẫn tùy chỉnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自定义路径" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自定义路径" } } } }, - "Cut": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cắt" + "Cut" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cắt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "剪切" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "剪切" } } } }, - "Dark": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tối" + "Dark" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "深色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "深色" } } } }, - "Data": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dữ liệu" + "Data" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据" } } } }, - "Data grid": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưới dữ liệu" + "Data grid" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưới dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据网格" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据网格" } } } }, - "Data Grid": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưới dữ liệu" + "Data Grid" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưới dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据网格" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据网格" } } } }, - "Data Grid Font": {}, - "Data Size": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích thước dữ liệu" + "Data Grid Font" : { + + }, + "Data Size" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích thước dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据大小" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据大小" } } } }, - "Data Type:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểu dữ liệu:" + "Data Type:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểu dữ liệu:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据类型:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据类型:" } } } }, - "Database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu" + "Database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库" } } } }, - "Database Driver": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trình điều khiển cơ sở dữ liệu" + "Database Driver" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trình điều khiển cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库驱动" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库驱动" } } } }, - "Database Drivers": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trình điều khiển cơ sở dữ liệu" + "Database Drivers" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trình điều khiển cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库驱动" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库驱动" } } } }, - "Database File": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tệp cơ sở dữ liệu" + "Database File" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tệp cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库文件" } } } }, - "Database file not found: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy tệp cơ sở dữ liệu: %@" + "Database file not found: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy tệp cơ sở dữ liệu: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到数据库文件: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到数据库文件: %@" } } } }, - "Database Index": {}, - "Database Index: %lld": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chỉ mục cơ sở dữ liệu: %lld" + "Database Index" : { + + }, + "Database Index: %lld" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chỉ mục cơ sở dữ liệu: %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库索引: %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库索引: %lld" } } } }, - "Database Name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên cơ sở dữ liệu" + "Database Name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库名称" } } } }, - "Database Switch Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển cơ sở dữ liệu thất bại" + "Database Switch Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển cơ sở dữ liệu thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换数据库失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换数据库失败" } } } }, - "Database Switcher": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển đổi cơ sở dữ liệu" + "Database Switcher" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển đổi cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库切换器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库切换器" } } } }, - "Database Type": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Loại Database" + "Database Type" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loại Database" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库类型" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库类型" } } } }, - "Database Type:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Loại cơ sở dữ liệu:" + "Database Type:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loại cơ sở dữ liệu:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库类型:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库类型:" } } } }, - "Database type: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Loại cơ sở dữ liệu: %@" + "Database type: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loại cơ sở dữ liệu: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库类型: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库类型: %@" } } } }, - "database_name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "database_name" + "database_name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "database_name" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "database_name" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "database_name" } } } }, - "Database: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu: %@" + "Database: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库: %@" } } } }, - "Database/Schema:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu/Schema:" + "Database/Schema:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu/Schema:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库/Schema:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库/Schema:" } } } }, - "Databases": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu" + "Databases" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据库" } } } }, - "Date format:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng ngày:" + "Date format:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng ngày:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "日期格式:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日期格式:" } } } }, - "Deactivate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy kích hoạt" + "Deactivate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy kích hoạt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "停用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停用" } } } }, - "Deactivate License?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy kích hoạt giấy phép?" + "Deactivate License?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy kích hoạt giấy phép?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "停用许可证?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停用许可证?" } } } }, - "Deactivate...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy kích hoạt..." + "Deactivate..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy kích hoạt..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "停用..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停用..." } } } }, - "Deactivated": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã hủy kích hoạt" + "Deactivated" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã hủy kích hoạt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已停用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已停用" } } } }, - "Deactivation Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy kích hoạt thất bại" + "Deactivation Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy kích hoạt thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "停用失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停用失败" } } } }, - "Decimal": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thập phân" + "Decimal" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thập phân" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "十进制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "十进制" } } } }, - "Default": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mặc định" + "Default" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认" } } } }, - "DEFAULT": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "MẶC ĐỊNH" + "DEFAULT" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "MẶC ĐỊNH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认" } } } }, - "Default Column": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột mặc định" + "Default Column" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认列" } } } }, - "Default connection policy": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chính sách kết nối mặc định" + "Default connection policy" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chính sách kết nối mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认连接策略" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认连接策略" } } } }, - "Default Operator": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Toán tử mặc định" + "Default Operator" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toán tử mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认运算符" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认运算符" } } } }, - "Default page size:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích thước trang mặc định:" + "Default page size:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích thước trang mặc định:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认页面大小:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认页面大小:" } } } }, - "Default Port": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cổng mặc định" + "Default Port" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cổng mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认端口" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认端口" } } } }, - "Default Port:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cổng mặc định:" + "Default Port:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cổng mặc định:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认端口:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认端口:" } } } }, - "Default value": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị mặc định" + "Default value" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认值" } } } }, - "Default Value": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị mặc định" + "Default Value" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认值" } } } }, - "Default:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mặc định:" + "Default:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mặc định:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "默认:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认:" } } } }, - "Delete": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa" + "Delete" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除" } } } }, - "Delete \"%@\"": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa \"%@\"" + "Delete \"%@\"" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa \"%@\"" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除「%@」" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除「%@」" } } } }, - "Delete (⌫)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa (⌫)" + "Delete (⌫)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa (⌫)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除 (⌫)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除 (⌫)" } } } }, - "Delete Check Constraint": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa ràng buộc kiểm tra" + "Delete Check Constraint" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa ràng buộc kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除检查约束" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除检查约束" } } } }, - "Delete Column": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa cột" + "Delete Column" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除列" } } } }, - "Delete Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa kết nối" + "Delete Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除连接" } } } }, - "Delete Folder": {}, - "Delete Folder?": {}, - "Delete Foreign Key": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa khóa ngoại" + "Delete Folder" : { + + }, + "Delete Folder?" : { + + }, + "Delete Foreign Key" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa khóa ngoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除外键" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除外键" } } } }, - "Delete Group": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa nhóm" + "Delete Group" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa nhóm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除分组" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除分组" } } } }, - "Delete Index": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa chỉ mục" + "Delete Index" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa chỉ mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除索引" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除索引" } } } }, - "Delete Preset": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa mẫu đặt trước" + "Delete Preset" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa mẫu đặt trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除预设" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除预设" } } } }, - "Delete Theme": {}, - "Deleted": {}, - "Deleted Text": {}, - "Delimiter": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dấu phân cách" + "Delete Theme" : { + + }, + "Deleted" : { + + }, + "Deleted Text" : { + + }, + "Delimiter" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dấu phân cách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分隔符" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分隔符" } } } }, - "Details": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chi tiết" + "Details" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chi tiết" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "详情" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "详情" } } } }, - "Digits": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số chữ số" + "Digits" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số chữ số" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "位数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "位数" } } } }, - "Disable foreign key checks": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tắt kiểm tra khóa ngoại" + "Disable foreign key checks" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tắt kiểm tra khóa ngoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "禁用外键检查" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "禁用外键检查" } } } }, - "Disabled": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã tắt" + "Disabled" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã tắt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已禁用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已禁用" } } } }, - "Discard": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy bỏ" + "Discard" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy bỏ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "丢弃" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "丢弃" } } } }, - "Discard Changes?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy bỏ thay đổi?" + "Discard Changes?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy bỏ thay đổi?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "丢弃更改?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "丢弃更改?" } } } }, - "Discard Unsaved Changes?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hủy bỏ thay đổi chưa lưu?" + "Discard Unsaved Changes?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hủy bỏ thay đổi chưa lưu?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "丢弃未保存的更改?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "丢弃未保存的更改?" } } } }, - "Disconnect": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ngắt kết nối" + "Disconnect" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngắt kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "断开连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "断开连接" } } } }, - "Disconnected": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã ngắt kết nối" + "Disconnected" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã ngắt kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已断开连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已断开连接" } } } }, - "Dismiss": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bỏ qua" + "Dismiss" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bỏ qua" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" } } } }, - "Display": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị" + "Display" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示" } } } }, - "Do you want to save changes?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có muốn lưu thay đổi?" + "Do you want to save changes?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có muốn lưu thay đổi?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "是否保存更改?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "是否保存更改?" } } } }, - "Documentation": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tài liệu" + "Documentation" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tài liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "文档" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文档" } } } }, - "Don't Allow": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không cho phép" + "Don't Allow" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không cho phép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不允许" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不允许" } } } }, - "Don't Save": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không lưu" + "Don't Save" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không lưu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不保存" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不保存" } } } }, - "Don't show this again": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không hiện lại" + "Don't show this again" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hiện lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不再显示" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不再显示" } } } }, - "Downloads": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lượt tải" + "Downloads" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lượt tải" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下载次数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下载次数" } } } }, - "Drop": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa" + "Drop" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除" } } } }, - "Drop %lld tables": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa %lld bảng" + "Drop %lld tables" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa %lld bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除 %lld 个表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除 %lld 个表" } } } }, - "Drop all tables that depend on this table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa tất cả bảng phụ thuộc vào bảng này" + "Drop all tables that depend on this table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa tất cả bảng phụ thuộc vào bảng này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除所有依赖此表的表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除所有依赖此表的表" } } } }, - "Drop table '%@'": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa bảng '%@'" + "Drop table '%@'" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa bảng '%@'" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除表 '%@'" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除表 '%@'" } } } }, - "Drop View": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa view" + "Drop View" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa view" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除视图" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除视图" } } } }, - "Duplicate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhân bản" + "Duplicate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhân bản" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" } } } }, - "Duplicate Existing Table": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhân bản bảng hiện có" + "Duplicate Existing Table" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhân bản bảng hiện có" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制现有表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制现有表" } } } }, - "Duplicate filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhân đôi bộ lọc" + "Duplicate filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhân đôi bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制筛选条件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制筛选条件" } } } }, - "Duplicate Filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhân bản bộ lọc" + "Duplicate Filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhân bản bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制筛选条件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制筛选条件" } } } }, - "Duplicate it to customize colors and layout.": {}, - "Duplicate Row": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhân bản dòng" + "Duplicate it to customize colors and layout." : { + + }, + "Duplicate Row" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhân bản dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制行" } } } }, - "Duplicate Table Structure": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhân bản cấu trúc bảng" + "Duplicate Table Structure" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhân bản cấu trúc bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制表结构" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制表结构" } } } }, - "Duplicate Theme": {}, - "Each SQLite file is a separate database.\nTo open a different database, create a new connection.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mỗi tệp SQLite là một cơ sở dữ liệu riêng.\nĐể mở cơ sở dữ liệu khác, hãy tạo kết nối mới." + "Duplicate Theme" : { + + }, + "Each SQLite file is a separate database.\nTo open a different database, create a new connection." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mỗi tệp SQLite là một cơ sở dữ liệu riêng.\nĐể mở cơ sở dữ liệu khác, hãy tạo kết nối mới." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每个 SQLite 文件是一个独立的数据库。\n要打开其他数据库,请创建新连接。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每个 SQLite 文件是一个独立的数据库。\n要打开其他数据库,请创建新连接。" } } } }, - "Edit": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa" + "Edit" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑" } } } }, - "Edit Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa kết nối" + "Edit Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑连接" } } } }, - "Edit Details (Double-click)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa chi tiết (Nhấp đúp)" + "Edit Details (Double-click)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa chi tiết (Nhấp đúp)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑详情(双击)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑详情(双击)" } } } }, - "Edit Provider": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa nhà cung cấp" + "Edit Provider" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa nhà cung cấp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑提供商" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑提供商" } } } }, - "Edit Row": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa dòng" + "Edit Row" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑行" } } } }, - "Edit View Definition": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa định nghĩa view" + "Edit View Definition" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa định nghĩa view" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑视图定义" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑视图定义" } } } }, - "Edit...": {}, - "Editing": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang sửa" + "Edit..." : { + + }, + "Editing" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang sửa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑中" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑中" } } } }, - "Editor": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trình soạn thảo" + "Editor" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trình soạn thảo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编辑器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编辑器" } } } }, - "Editor Font": {}, - "Email:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Email:" + "Editor Font" : { + + }, + "Email:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Email:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "邮箱:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "邮箱:" } } } }, - "Empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trống" + "Empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "空" } } } }, - "Empty Redis command": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lệnh Redis trống" + "Empty Redis command" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lệnh Redis trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Redis 命令为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redis 命令为空" } } } }, - "Enable %@": {}, - "Enable inline suggestions": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật gợi ý trực tiếp" + "Enable %@" : { + + }, + "Enable inline suggestions" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật gợi ý trực tiếp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启用行内建议" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用行内建议" } } } }, - "Enable preview tabs": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật tab xem trước" + "Enable preview tabs" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật tab xem trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启用预览标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用预览标签页" } } } }, - "Enable SSH Tunnel": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật đường hầm SSH" + "Enable SSH Tunnel" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật đường hầm SSH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启用 SSH 隧道" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用 SSH 隧道" } } } }, - "Enabled": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã bật" + "Enabled" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã bật" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已启用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已启用" } } } }, - "Encoding:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mã hóa:" + "Encoding:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mã hóa:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "编码:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "编码:" } } } }, - "Endpoint": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Endpoint" + "Endpoint" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Endpoint" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Endpoint" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Endpoint" } } } }, - "ends with": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "kết thúc bằng" + "ends with" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "kết thúc bằng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "以...结尾" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "以...结尾" } } } }, - "Engine": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Engine" + "Engine" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engine" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Engine" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engine" } } } }, - "Engine (e.g., InnoDB)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Engine (vd: InnoDB)" + "Engine (e.g., InnoDB)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engine (vd: InnoDB)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Engine(如 InnoDB)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engine(如 InnoDB)" } } } }, - "Enter a name for this filter preset": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập tên cho mẫu bộ lọc này" + "Enter a name for this filter preset" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập tên cho mẫu bộ lọc này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "输入此筛选预设的名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入此筛选预设的名称" } } } }, - "Enter a new name for the group.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập tên mới cho nhóm." + "Enter a new name for the group." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập tên mới cho nhóm." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "输入分组的新名称。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入分组的新名称。" } } } }, - "Enter database name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập tên cơ sở dữ liệu" + "Enter database name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập tên cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "输入数据库名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入数据库名称" } } } }, - "Enter the TOTP verification code for SSH authentication.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập mã xác minh TOTP để xác thực SSH." + "Enter the TOTP verification code for SSH authentication." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập mã xác minh TOTP để xác thực SSH." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "输入 TOTP 验证码以进行 SSH 身份验证。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入 TOTP 验证码以进行 SSH 身份验证。" } } } }, - "equals": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "bằng" + "equals" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "bằng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "等于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "等于" } } } }, - "Error": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi" + "Error" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "错误" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "错误" } } } }, - "Error Applying Changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi áp dụng thay đổi" + "Error Applying Changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi áp dụng thay đổi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "应用更改时出错" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用更改时出错" } } } }, - "Error:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi:" + "Error:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "错误:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "错误:" } } } }, - "Error: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi: %@" + "Error: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "错误: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "错误: %@" } } } }, - "Error: Selected path is not a regular file": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi: Đường dẫn đã chọn không phải tệp thông thường" + "Error: Selected path is not a regular file" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi: Đường dẫn đã chọn không phải tệp thông thường" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "错误: 所选路径不是常规文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "错误: 所选路径不是常规文件" } } } }, - "EU Long (31/12/2024 23:59:59)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Châu Âu dài (31/12/2024 23:59:59)" + "EU Long (31/12/2024 23:59:59)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Châu Âu dài (31/12/2024 23:59:59)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "欧洲长格式 (31/12/2024 23:59:59)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "欧洲长格式 (31/12/2024 23:59:59)" } } } }, - "EU Short (31/12/2024)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Châu Âu ngắn (31/12/2024)" + "EU Short (31/12/2024)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Châu Âu ngắn (31/12/2024)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "欧洲短格式 (31/12/2024)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "欧洲短格式 (31/12/2024)" } } } }, - "Every table needs at least one column. Click + to get started": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mỗi bảng cần ít nhất một cột. Nhấn + để bắt đầu" + "Every table needs at least one column. Click + to get started" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mỗi bảng cần ít nhất một cột. Nhấn + để bắt đầu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每个表至少需要一列。点击 + 开始添加" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每个表至少需要一列。点击 + 开始添加" } } } }, - "Execute": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thực thi" + "Execute" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thực thi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "执行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "执行" } } } }, - "Execute All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thực thi tất cả" + "Execute All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thực thi tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "全部执行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部执行" } } } }, - "Execute all statements in a single transaction. If any statement fails, all changes are rolled back.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thực thi tất cả câu lệnh trong một giao dịch. Nếu bất kỳ câu lệnh nào thất bại, tất cả thay đổi sẽ được hoàn tác." + "Execute all statements in a single transaction. If any statement fails, all changes are rolled back." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thực thi tất cả câu lệnh trong một giao dịch. Nếu bất kỳ câu lệnh nào thất bại, tất cả thay đổi sẽ được hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在单个事务中执行所有语句。如果任一语句失败,所有更改将回滚。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在单个事务中执行所有语句。如果任一语句失败,所有更改将回滚。" } } } }, - "Execute Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thực thi truy vấn" + "Execute Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thực thi truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "执行查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "执行查询" } } } }, - "Executed %lld statements": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã thực thi %lld câu lệnh" + "Executed %lld statements" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã thực thi %lld câu lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已执行 %lld 条语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已执行 %lld 条语句" } } } }, - "Executing": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang thực thi" + "Executing" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang thực thi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "执行中" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "执行中" } } } }, - "Executing...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang thực thi..." + "Executing..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang thực thi..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在执行..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在执行..." } } } }, - "Expired": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hết hạn" + "Expired" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hết hạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已过期" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已过期" } } } }, - "Explain": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giải thích" + "Explain" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giải thích" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "解释" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "解释" } } } }, - "EXPLAIN is not supported for this database type.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "EXPLAIN không được hỗ trợ cho loại cơ sở dữ liệu này." + "EXPLAIN is not supported for this database type." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "EXPLAIN không được hỗ trợ cho loại cơ sở dữ liệu này." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此数据库类型不支持 EXPLAIN。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此数据库类型不支持 EXPLAIN。" } } } }, - "Explain Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giải thích truy vấn" + "Explain Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giải thích truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "解释查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "解释查询" } } } }, - "Explain this SQL query:\n\n```sql\n%@\n```": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giải thích chi tiết câu truy vấn SQL sau:\n\n```sql\n%@\n```" + "Explain this SQL query:\n\n```sql\n%@\n```" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giải thích chi tiết câu truy vấn SQL sau:\n\n```sql\n%@\n```" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "请解释以下 SQL 查询:\n\n```sql\n%@\n```" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请解释以下 SQL 查询:\n\n```sql\n%@\n```" } } } }, - "Explain with AI": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giải thích với AI" + "Explain with AI" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giải thích với AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI 解释" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 解释" } } } }, - "export": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "xuất" + "export" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "xuất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出" } } } }, - "Export": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất" + "Export" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出" } } } }, - "Export completed successfully": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất dữ liệu thành công" + "Export completed successfully" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất dữ liệu thành công" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出成功" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出成功" } } } }, - "Export data": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất dữ liệu" + "Export data" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出数据" } } } }, - "Export Data": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất dữ liệu" + "Export Data" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出数据" } } } }, - "Export Data (⌘⇧E)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất dữ liệu (⌘⇧E)" + "Export Data (⌘⇧E)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất dữ liệu (⌘⇧E)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出数据 (⌘⇧E)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出数据 (⌘⇧E)" } } } }, - "Export Error": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi xuất dữ liệu" + "Export Error" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi xuất dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出错误" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出错误" } } } }, - "Export failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất thất bại: %@" + "Export failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出失败: %@" } } } }, - "Export Format": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng xuất" + "Export Format" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng xuất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出格式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出格式" } } } }, - "Export format '%@' not found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy định dạng xuất '%@'" + "Export format '%@' not found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy định dạng xuất '%@'" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到导出格式 '%@'" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到导出格式 '%@'" } } } }, - "Export Formats": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng xuất" + "Export Formats" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng xuất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出格式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出格式" } } } }, - "Export multiple tables": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất nhiều bảng" + "Export multiple tables" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất nhiều bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出多个表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出多个表" } } } }, - "Export table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất bảng" + "Export table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出表" } } } }, - "Export...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất..." + "Export..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导出..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出..." } } } }, - "Exports data as mongosh-compatible scripts. Drop, Indexes, and Data options are configured per collection in the collection list.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuất dữ liệu dưới dạng script tương thích mongosh. Tùy chọn Drop, Indexes và Data được cấu hình cho từng collection trong danh sách collection." + "Exports data as mongosh-compatible scripts. Drop, Indexes, and Data options are configured per collection in the collection list." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất dữ liệu dưới dạng script tương thích mongosh. Tùy chọn Drop, Indexes và Data được cấu hình cho từng collection trong danh sách collection." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将数据导出为 mongosh 兼容的脚本。Drop、Indexes 和 Data 选项可在集合列表中按集合配置。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将数据导出为 mongosh 兼容的脚本。Drop、Indexes 和 Data 选项可在集合列表中按集合配置。" } } } }, - "Expression (e.g., age >= 0)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Biểu thức (vd: age >= 0)" + "Expression (e.g., age >= 0)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Biểu thức (vd: age >= 0)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表达式(如 age >= 0)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表达式(如 age >= 0)" } } } }, - "Extra Large": {}, - "Failed at line %lld": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thất bại tại dòng %lld" + "Extra Large" : { + + }, + "Failed at line %lld" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thất bại tại dòng %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在第 %lld 行失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在第 %lld 行失败" } } } }, - "Failed to compress data": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể nén dữ liệu" + "Failed to compress data" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể nén dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "压缩数据失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "压缩数据失败" } } } }, - "Failed to decompress .gz file": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giải nén tệp .gz thất bại" + "Failed to decompress .gz file" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giải nén tệp .gz thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "解压 .gz 文件失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "解压 .gz 文件失败" } } } }, - "Failed to decompress file: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giải nén tệp thất bại: %@" + "Failed to decompress file: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giải nén tệp thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "解压文件失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "解压文件失败: %@" } } } }, - "Failed to delete template: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa mẫu thất bại: %@" + "Failed to delete template: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa mẫu thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除模板失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除模板失败: %@" } } } }, - "Failed to encode content as UTF-8": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể mã hóa nội dung thành UTF-8" + "Failed to encode content as UTF-8" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể mã hóa nội dung thành UTF-8" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法将内容编码为 UTF-8" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法将内容编码为 UTF-8" } } } }, - "Failed to encode sync data: %@": {}, - "Failed to fetch table structure: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lấy cấu trúc bảng thất bại: %@" + "Failed to encode sync data: %@" : { + + }, + "Failed to fetch table structure: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lấy cấu trúc bảng thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "获取表结构失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "获取表结构失败: %@" } } } }, - "Failed to import DDL: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập DDL thất bại: %@" + "Failed to import DDL: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập DDL thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入 DDL 失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入 DDL 失败: %@" } } } }, - "Failed to Load": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải thất bại" + "Failed to Load" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载失败" } } } }, - "Failed to load databases": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải danh sách cơ sở dữ liệu thất bại" + "Failed to load databases" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải danh sách cơ sở dữ liệu thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载数据库列表失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载数据库列表失败" } } } }, - "Failed to load databases: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải danh sách cơ sở dữ liệu thất bại: %@" + "Failed to load databases: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải danh sách cơ sở dữ liệu thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载数据库列表失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载数据库列表失败: %@" } } } }, - "Failed to load plugin registry": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể tải danh sách plugin" + "Failed to load plugin registry" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể tải danh sách plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法加载插件注册表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法加载插件注册表" } } } }, - "Failed to load preview using encoding: %@. Try selecting a different text encoding from the encoding picker and reload the preview.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải bản xem trước thất bại với mã hóa: %@. Hãy thử chọn mã hóa văn bản khác và tải lại bản xem trước." + "Failed to load preview using encoding: %@. Try selecting a different text encoding from the encoding picker and reload the preview." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải bản xem trước thất bại với mã hóa: %@. Hãy thử chọn mã hóa văn bản khác và tải lại bản xem trước." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "使用编码 %@ 加载预览失败。请尝试从编码选择器中选择其他文本编码并重新加载预览。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用编码 %@ 加载预览失败。请尝试从编码选择器中选择其他文本编码并重新加载预览。" } } } }, - "Failed to load preview using encoding: %@. Try selecting a different text encoding.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể tải xem trước với mã hóa: %@. Hãy thử chọn mã hóa văn bản khác." + "Failed to load preview using encoding: %@. Try selecting a different text encoding." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể tải xem trước với mã hóa: %@. Hãy thử chọn mã hóa văn bản khác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法使用编码 %@ 加载预览。请尝试选择其他文本编码。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法使用编码 %@ 加载预览。请尝试选择其他文本编码。" } } } }, - "Failed to load preview: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải bản xem trước thất bại: %@" + "Failed to load preview: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải bản xem trước thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载预览失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载预览失败: %@" } } } }, - "Failed to load schemas": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể tải danh sách schema" + "Failed to load schemas" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể tải danh sách schema" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法加载 Schema 列表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法加载 Schema 列表" } } } }, - "Failed to load tables: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải danh sách bảng thất bại: %@" + "Failed to load tables: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải danh sách bảng thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载表列表失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载表列表失败: %@" } } } }, - "Failed to load template: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải mẫu thất bại: %@" + "Failed to load template: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải mẫu thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载模板失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载模板失败: %@" } } } }, - "Failed to open SSH channel for port forwarding": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể mở kênh SSH để chuyển tiếp cổng" + "Failed to open SSH channel for port forwarding" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể mở kênh SSH để chuyển tiếp cổng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法打开 SSH 通道进行端口转发" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法打开 SSH 通道进行端口转发" } } } }, - "Failed to parse any columns from table '%@'. Check console for debug info.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể phân tích cột từ bảng '%@'. Kiểm tra console để xem thông tin gỡ lỗi." + "Failed to parse any columns from table '%@'. Check console for debug info." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể phân tích cột từ bảng '%@'. Kiểm tra console để xem thông tin gỡ lỗi." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法从表 '%@' 解析任何列。请检查控制台获取调试信息。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法从表 '%@' 解析任何列。请检查控制台获取调试信息。" } } } }, - "Failed to parse plugin registry": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể phân tích danh sách plugin" + "Failed to parse plugin registry" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể phân tích danh sách plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法解析插件注册表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法解析插件注册表" } } } }, - "Failed to parse statement at line %lld: %@": { - "extractionState": "stale", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Failed to parse statement at line %1$lld: %2$@" + "Failed to parse statement at line %lld: %@" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Failed to parse statement at line %1$lld: %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phân tích câu lệnh thất bại tại dòng %1$lld: %2$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phân tích câu lệnh thất bại tại dòng %1$lld: %2$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在第 %lld 行解析语句失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在第 %lld 行解析语句失败: %@" } } } }, - "Failed to read file: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đọc tệp thất bại: %@" + "Failed to read file: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đọc tệp thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "读取文件失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "读取文件失败: %@" } } } }, - "Failed to Save Changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu thay đổi thất bại" + "Failed to Save Changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu thay đổi thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存更改失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存更改失败" } } } }, - "Failed to save template: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu mẫu thất bại: %@" + "Failed to save template: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu mẫu thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存模板失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存模板失败: %@" } } } }, - "Failed to write file: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể ghi tệp: %@" + "Failed to write file: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể ghi tệp: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "写入文件失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "写入文件失败: %@" } } } }, - "false": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "false" + "false" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "false" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "false" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "false" } } } }, - "FALSE": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "FALSE" + "FALSE" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "FALSE" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "FALSE" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "FALSE" } } } }, - "Family": {}, - "Fast": {}, - "Favorites": {}, - "Feature Routing": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định tuyến tính năng" + "Family" : { + + }, + "Fast" : { + + }, + "Favorites" : { + + }, + "Feature Routing" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định tuyến tính năng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "功能路由" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "功能路由" } } } }, - "FIELDS": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TRƯỜNG" + "FIELDS" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TRƯỜNG" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "字段" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字段" } } } }, - "FIELDS (%lld)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CÁC TRƯỜNG (%lld)" + "FIELDS (%lld)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CÁC TRƯỜNG (%lld)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "字段 (%lld)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字段 (%lld)" } } } }, - "File": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tệp" + "File" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件" } } } }, - "File name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên tệp" + "File name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "文件名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件名" } } } }, - "File not found": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy tệp" + "File not found" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到文件" } } } }, - "File Path": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đường dẫn tệp" + "File Path" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đường dẫn tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "文件路径" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件路径" } } } }, - "Filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bộ lọc" + "Filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选" } } } }, - "Filter column: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột lọc: %@" + "Filter column: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột lọc: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选列: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选列: %@" } } } }, - "Filter favorites": {}, - "Filter logic mode": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ logic bộ lọc" + "Filter favorites" : { + + }, + "Filter logic mode" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ logic bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选逻辑模式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选逻辑模式" } } } }, - "Filter operator: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Toán tử lọc: %@" + "Filter operator: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toán tử lọc: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选运算符: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选运算符: %@" } } } }, - "Filter presets": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt sẵn bộ lọc" + "Filter presets" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt sẵn bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选预设" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选预设" } } } }, - "Filter settings": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt bộ lọc" + "Filter settings" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选设置" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选设置" } } } }, - "Filter Settings": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt bộ lọc" + "Filter Settings" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选设置" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选设置" } } } }, - "Filter with column": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lọc theo cột" + "Filter with column" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lọc theo cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "按列筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按列筛选" } } } }, - "Filter...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lọc..." + "Filter..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lọc..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选..." } } } }, - "Filters": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bộ lọc" + "Filters" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "筛选条件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "筛选条件" } } } }, - "Fix Error": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sửa lỗi" + "Fix Error" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sửa lỗi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "修复错误" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "修复错误" } } } }, - "Focus Border": {}, - "Folder name": {}, - "Font": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phông chữ" + "Focus Border" : { + + }, + "Folder name" : { + + }, + "Font" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phông chữ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "字体" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字体" } } } }, - "Font:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phông chữ:" + "Font:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phông chữ:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "字体:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字体:" } } } }, - "Fonts": {}, - "Forever": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mãi mãi" + "Fonts" : { + + }, + "Forever" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mãi mãi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "永久" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "永久" } } } }, - "Format Query (⌥⌘F)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng truy vấn (⌥⌘F)" + "Format Query (⌥⌘F)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng truy vấn (⌥⌘F)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "格式化查询 (⌥⌘F)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式化查询 (⌥⌘F)" } } } }, - "Format:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng:" + "Format:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "格式:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式:" } } } }, - "Formatter error: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi định dạng: %@" + "Formatter error: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi định dạng: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "格式化错误: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式化错误: %@" } } } }, - "Formatting not supported for %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không hỗ trợ định dạng cho %@" + "Formatting not supported for %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hỗ trợ định dạng cho %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持 %@ 的格式化" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持 %@ 的格式化" } } } }, - "Function": {}, - "General": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tổng quát" + "Function" : { + + }, + "General" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng quát" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "通用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通用" } } } }, - "Generated WHERE Clause": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mệnh đề WHERE đã tạo" + "Generated WHERE Clause" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mệnh đề WHERE đã tạo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "生成的 WHERE 子句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "生成的 WHERE 子句" } } } }, - "Generation failed.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo phản hồi thất bại." + "Generation failed." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo phản hồi thất bại." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "生成失败。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "生成失败。" } } } }, - "Get help writing queries, explaining schemas, or fixing errors.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhận trợ giúp viết truy vấn, giải thích schema hoặc sửa lỗi." + "Get help writing queries, explaining schemas, or fixing errors." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhận trợ giúp viết truy vấn, giải thích schema hoặc sửa lỗi." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "获取编写查询、解释 Schema 或修复错误方面的帮助。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "获取编写查询、解释 Schema 或修复错误方面的帮助。" } } } }, - "Get Started": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bắt đầu" + "Get Started" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bắt đầu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "开始使用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开始使用" } } } }, - "GitHub Repository": {}, - "Global:": {}, - "Go": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đi" + "GitHub Repository" : { + + }, + "Global:" : { + + }, + "Go" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "前往" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "前往" } } } }, - "Go to Settings…": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đi đến Cài đặt…" + "Go to Settings…" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đi đến Cài đặt…" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "前往设置…" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "前往设置…" } } } }, - "Graphite": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Than chì" + "Graphite" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Than chì" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "石墨" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "石墨" } } } }, - "Gray": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xám" + "Gray" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xám" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "灰色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "灰色" } } } }, - "greater or equal": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "lớn hơn hoặc bằng" + "greater or equal" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "lớn hơn hoặc bằng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "大于或等于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大于或等于" } } } }, - "greater than": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "lớn hơn" + "greater than" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "lớn hơn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "大于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大于" } } } }, - "Green": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xanh lá" + "Green" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xanh lá" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "绿色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "绿色" } } } }, - "Group": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhóm" + "Group" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhóm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分组" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分组" } } } }, - "Group name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên nhóm" + "Group name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên nhóm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分组名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分组名称" } } } }, - "Groups & Tags:": {}, - "Help improve TablePro by sharing anonymous usage statistics (no personal data or queries).": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giúp cải thiện TablePro bằng cách chia sẻ thống kê sử dụng ẩn danh (không có dữ liệu cá nhân hay truy vấn nào)." + "Groups & Tags:" : { + + }, + "Help improve TablePro by sharing anonymous usage statistics (no personal data or queries)." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giúp cải thiện TablePro bằng cách chia sẻ thống kê sử dụng ẩn danh (không có dữ liệu cá nhân hay truy vấn nào)." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "通过分享匿名使用统计数据帮助改进 TablePro(不包含个人数据或查询内容)。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通过分享匿名使用统计数据帮助改进 TablePro(不包含个人数据或查询内容)。" } } } }, - "Hex bytes": {}, - "Hide All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ẩn tất cả" + "Hex bytes" : { + + }, + "Hide All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ẩn tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "隐藏全部" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏全部" } } } }, - "Hide Column": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ẩn cột" + "Hide Column" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ẩn cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "隐藏列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏列" } } } }, - "Higher values create fewer INSERT statements, resulting in smaller files and faster imports": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị cao hơn tạo ít câu lệnh INSERT hơn, giúp tệp nhỏ hơn và nhập nhanh hơn" + "Higher values create fewer INSERT statements, resulting in smaller files and faster imports" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị cao hơn tạo ít câu lệnh INSERT hơn, giúp tệp nhỏ hơn và nhập nhanh hơn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "值越大,生成的 INSERT 语句越少,文件更小,导入更快" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "值越大,生成的 INSERT 语句越少,文件更小,导入更快" } } } }, - "Highlight current line": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đánh dấu dòng hiện tại" + "Highlight current line" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đánh dấu dòng hiện tại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "高亮当前行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高亮当前行" } } } }, - "History": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lịch sử" + "History" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lịch sử" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "历史记录" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "历史记录" } } } }, - "history entries": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "mục lịch sử" + "history entries" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "mục lịch sử" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "条历史记录" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "条历史记录" } } } }, - "history entry": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "mục lịch sử" + "history entry" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "mục lịch sử" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "条历史记录" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "条历史记录" } } } }, - "History Limit:": {}, - "Homepage": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trang chủ" + "History Limit:" : { + + }, + "Homepage" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trang chủ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "主页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主页" } } } }, - "Host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Máy chủ" + "Host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máy chủ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "主机" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主机" } } } }, - "Hover": {}, - "Huge": {}, - "iCloud account is not available. Sign in to iCloud in System Settings.": {}, - "iCloud Connected": {}, - "iCloud server error: %@": {}, - "iCloud storage is full. Free up space or reduce the history sync limit.": {}, - "iCloud Sync": {}, - "iCloud Sync is active": {}, - "iCloud Sync:": {}, - "Icon Sizes": {}, - "Ignore foreign key checks": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bỏ qua kiểm tra khóa ngoại" + "Hover" : { + + }, + "Huge" : { + + }, + "iCloud account is not available. Sign in to iCloud in System Settings." : { + + }, + "iCloud Connected" : { + + }, + "iCloud server error: %@" : { + + }, + "iCloud storage is full. Free up space or reduce the history sync limit." : { + + }, + "iCloud Sync" : { + + }, + "iCloud Sync is active" : { + + }, + "iCloud Sync:" : { + + }, + "Icon Sizes" : { + + }, + "Ignore foreign key checks" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bỏ qua kiểm tra khóa ngoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "忽略外键检查" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "忽略外键检查" } } } }, - "Import": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập" + "Import" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入" } } } }, - "Import cancelled by user": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Người dùng đã hủy nhập" + "Import cancelled by user" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Người dùng đã hủy nhập" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "用户已取消导入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户已取消导入" } } } }, - "Import data": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập dữ liệu" + "Import data" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入数据" } } } }, - "Import Data": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập dữ liệu" + "Import Data" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入数据" } } } }, - "Import Data (⌘⇧I)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập dữ liệu (⌘⇧I)" + "Import Data (⌘⇧I)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập dữ liệu (⌘⇧I)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入数据 (⌘⇧I)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入数据 (⌘⇧I)" } } } }, - "Import Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập thất bại" + "Import Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入失败" } } } }, - "Import failed at line %lld: %@": { - "extractionState": "stale", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Import failed at line %1$lld: %2$@" + "Import failed at line %lld: %@" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Import failed at line %1$lld: %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập thất bại tại dòng %1$lld: %2$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập thất bại tại dòng %1$lld: %2$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在第 %lld 行导入失败: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在第 %lld 行导入失败: %@" } } } }, - "Import Format": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng nhập" + "Import Format" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng nhập" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入格式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入格式" } } } }, - "Import Formats": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng nhập" + "Import Formats" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng nhập" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入格式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入格式" } } } }, - "Import from DDL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập từ DDL" + "Import from DDL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập từ DDL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从 DDL 导入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从 DDL 导入" } } } }, - "Import from URL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập từ URL" + "Import from URL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập từ URL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从 URL 导入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从 URL 导入" } } } }, - "Import Not Supported": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không hỗ trợ nhập" + "Import Not Supported" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hỗ trợ nhập" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持导入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持导入" } } } }, - "Import SQL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập SQL" + "Import SQL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入 SQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入 SQL" } } } }, - "Import Successful": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập thành công" + "Import Successful" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập thành công" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入成功" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入成功" } } } }, - "Import...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập..." + "Import..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入..." } } } }, - "Importing...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang nhập..." + "Importing..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang nhập..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入中..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入中..." } } } }, - "in list": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "trong danh sách" + "in list" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "trong danh sách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在列表中" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在列表中" } } } }, - "Include column headers": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bao gồm tiêu đề cột" + "Include column headers" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bao gồm tiêu đề cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含列标题" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含列标题" } } } }, - "Include current query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bao gồm truy vấn hiện tại" + "Include current query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bao gồm truy vấn hiện tại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含当前查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含当前查询" } } } }, - "Include database schema": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bao gồm schema cơ sở dữ liệu" + "Include database schema" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bao gồm schema cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含数据库 Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含数据库 Schema" } } } }, - "Include NULL values": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bao gồm giá trị NULL" + "Include NULL values" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bao gồm giá trị NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含 NULL 值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含 NULL 值" } } } }, - "Include query results": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bao gồm kết quả truy vấn" + "Include query results" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bao gồm kết quả truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包含查询结果" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包含查询结果" } } } }, - "INDEX": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CHỈ MỤC" + "INDEX" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CHỈ MỤC" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "索引" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "索引" } } } }, - "Index name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên chỉ mục" + "Index name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên chỉ mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "索引名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "索引名称" } } } }, - "Index Size": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích thước chỉ mục" + "Index Size" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích thước chỉ mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "索引大小" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "索引大小" } } } }, - "Indexes": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chỉ mục" + "Indexes" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chỉ mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "索引" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "索引" } } } }, - "Info": {}, - "Inline Suggestions": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gợi ý nội tuyến" + "Info" : { + + }, + "Inline Suggestions" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gợi ý nội tuyến" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行内建议" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行内建议" } } } }, - "Insert": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chèn" + "Insert" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chèn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插入" } } } }, - "Insert in Editor": {}, - "INSERT Statement(s)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Câu lệnh INSERT" + "Insert in Editor" : { + + }, + "INSERT Statement(s)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Câu lệnh INSERT" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "INSERT 语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "INSERT 语句" } } } }, - "Inserted": {}, - "Inspector": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thanh kiểm tra" + "Inserted" : { + + }, + "Inspector" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thanh kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "检查器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "检查器" } } } }, - "Install": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt" + "Install" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安装" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装" } } } }, - "Install from File...": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt từ tập tin..." + "Install from File..." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt từ tập tin..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从文件安装..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从文件安装..." } } } }, - "Install Plugin": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt Plugin" + "Install Plugin" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt Plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安装插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装插件" } } } }, - "Install plugin from file": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt plugin từ tệp" + "Install plugin from file" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt plugin từ tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从文件安装插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从文件安装插件" } } } }, - "Install Theme": {}, - "Installation Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt thất bại" + "Install Theme" : { + + }, + "Installation Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安装失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装失败" } } } }, - "Installed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã cài đặt" + "Installed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已安装" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已安装" } } } }, - "Installed Plugins": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin đã cài đặt" + "Installed Plugins" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin đã cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已安装的插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已安装的插件" } } } }, - "Installing...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang cài đặt..." + "Installing..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang cài đặt..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安装中..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装中..." } } } }, - "Interface": {}, - "Invalid argument: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đối số không hợp lệ: %@" + "Interface" : { + + }, + "Invalid argument: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đối số không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无效的参数: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的参数: %@" } } } }, - "Invalid connection URL format": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng URL kết nối không hợp lệ" + "Invalid connection URL format" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng URL kết nối không hợp lệ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接 URL 格式无效" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接 URL 格式无效" } } } }, - "Invalid data format: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng dữ liệu không hợp lệ: %@" + "Invalid data format: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng dữ liệu không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "数据格式无效: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据格式无效: %@" } } } }, - "Invalid endpoint: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Endpoint không hợp lệ: %@" + "Invalid endpoint: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Endpoint không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无效的 Endpoint: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的 Endpoint: %@" } } } }, - "Invalid file encoding. Try a different encoding option.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mã hóa tệp không hợp lệ. Hãy thử tùy chọn mã hóa khác." + "Invalid file encoding. Try a different encoding option." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mã hóa tệp không hợp lệ. Hãy thử tùy chọn mã hóa khác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "文件编码无效。请尝试其他编码选项。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件编码无效。请尝试其他编码选项。" } } } }, - "Invalid hex": {}, - "Invalid JSON": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "JSON không hợp lệ" + "Invalid hex" : { + + }, + "Invalid JSON" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON không hợp lệ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无效的 JSON" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的 JSON" } } } }, - "Invalid JSON: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "JSON không hợp lệ: %@" + "Invalid JSON: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无效的 JSON: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的 JSON: %@" } } } }, - "Invalid MongoDB syntax: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cú pháp MongoDB không hợp lệ: %@" + "Invalid MongoDB syntax: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cú pháp MongoDB không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无效的 MongoDB 语法: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的 MongoDB 语法: %@" } } } }, - "Invalid plugin bundle: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin bundle không hợp lệ: %@" + "Invalid plugin bundle: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin bundle không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无效的插件包: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的插件包: %@" } } } }, - "Invalid username or password": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên đăng nhập hoặc mật khẩu không hợp lệ" + "Invalid username or password" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên đăng nhập hoặc mật khẩu không hợp lệ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "用户名或密码无效" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户名或密码无效" } } } }, - "Invisibles": {}, - "is empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "trống" + "Invisibles" : { + + }, + "is empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "为空" } } } }, - "is not empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "không trống" + "is not empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "không trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不为空" } } } }, - "is not NULL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "không phải NULL" + "is not NULL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "không phải NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不为 NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不为 NULL" } } } }, - "is NULL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "là NULL" + "is NULL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "là NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "为 NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "为 NULL" } } } }, - "ISO 8601 (2024-12-31 23:59:59)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "ISO 8601 (2024-12-31 23:59:59)" + "ISO 8601 (2024-12-31 23:59:59)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ISO 8601 (2024-12-31 23:59:59)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "ISO 8601 (2024-12-31 23:59:59)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ISO 8601 (2024-12-31 23:59:59)" } } } }, - "ISO Date (2024-12-31)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "ISO ngày (2024-12-31)" + "ISO Date (2024-12-31)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ISO ngày (2024-12-31)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "ISO 日期 (2024-12-31)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ISO 日期 (2024-12-31)" } } } }, - "Items": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mục" + "Items" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "项目" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "项目" } } } }, - "JSON": {}, - "Jump Hosts": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Jump Host" + "JSON" : { + + }, + "Jump Hosts" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jump Host" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Jump Host" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jump Host" } } } }, - "Jump hosts are connected in order before reaching the SSH server above. Only key and agent auth are supported for jumps.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Jump host được kết nối theo thứ tự trước khi đến SSH server phía trên. Chỉ hỗ trợ xác thực bằng khoá và agent cho các jump." + "Jump hosts are connected in order before reaching the SSH server above. Only key and agent auth are supported for jumps." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jump host được kết nối theo thứ tự trước khi đến SSH server phía trên. Chỉ hỗ trợ xác thực bằng khoá và agent cho các jump." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Jump Host 按顺序连接后再连接到上方的 SSH 服务器。跳转仅支持密钥和 Agent 认证。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jump Host 按顺序连接后再连接到上方的 SSH 服务器。跳转仅支持密钥和 Agent 认证。" } } } }, - "Keep entries for:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giữ mục trong:" + "Keep entries for:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giữ mục trong:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保留记录:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留记录:" } } } }, - "Keep leading zeros in ZIP codes, phone numbers, and IDs by outputting all values as strings": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giữ số 0 đầu trong mã bưu chính, số điện thoại và ID bằng cách xuất tất cả giá trị dưới dạng chuỗi" + "Keep leading zeros in ZIP codes, phone numbers, and IDs by outputting all values as strings" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giữ số 0 đầu trong mã bưu chính, số điện thoại và ID bằng cách xuất tất cả giá trị dưới dạng chuỗi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "通过将所有值输出为字符串来保留邮编、电话号码和 ID 中的前导零" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通过将所有值输出为字符串来保留邮编、电话号码和 ID 中的前导零" } } } }, - "Keep Other Version": {}, - "Keep This Mac's Version": {}, - "Key File": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tệp khóa" + "Keep Other Version" : { + + }, + "Keep This Mac's Version" : { + + }, + "Key File" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tệp khóa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密钥文件" + } + } + } + }, + "Key Prefix Root" : { + + }, + "Keyboard" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bàn phím" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "密钥文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "键盘" } } } }, - "Keyboard": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bàn phím" + "Keyboard Interactive" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keyboard Interactive" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "键盘" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keyboard Interactive" } } } }, - "Keyboard Interactive": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Keyboard Interactive" + "Keys are provided by the SSH agent (e.g. 1Password, ssh-agent)." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khóa được cung cấp bởi SSH agent (ví dụ: 1Password, ssh-agent)." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Keyboard Interactive" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密钥由 SSH Agent 提供(如 1Password、ssh-agent)。" } } } }, - "Keys are provided by the SSH agent (e.g. 1Password, ssh-agent).": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khóa được cung cấp bởi SSH agent (ví dụ: 1Password, ssh-agent)." + "Keyword" : { + + }, + "Keyword cannot contain spaces" : { + + }, + "Keyword:" : { + + }, + "keyword: %@" : { + + }, + "Language:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngôn ngữ:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "密钥由 SSH Agent 提供(如 1Password、ssh-agent)。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言:" } } } }, - "Keyword": {}, - "Keyword cannot contain spaces": {}, - "Keyword:": {}, - "keyword: %@": {}, - "Language:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ngôn ngữ:" + "Large" : { + + }, + "Last query execution summary" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng kết thực thi truy vấn gần nhất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "语言:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次查询执行摘要" } } } }, - "Large": {}, - "Last query execution summary": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tổng kết thực thi truy vấn gần nhất" + "Last query execution time" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thời gian thực thi truy vấn gần nhất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上次查询执行摘要" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次查询执行时间" } } } }, - "Last query execution time": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thời gian thực thi truy vấn gần nhất" + "Last query took %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn trước mất %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上次查询执行时间" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次查询耗时 %@" } } } }, - "Last query took %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn trước mất %@" + "Last query: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn gần nhất: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上次查询耗时 %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上次查询: %@" } } } }, - "Last query: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn gần nhất: %@" + "Last synced %@" : { + + }, + "Last Synced:" : { + + }, + "Latency: %lldms" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ trễ: %lldms" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上次查询: %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "延迟: %lldms" } } } }, - "Last synced %@": {}, - "Last Synced:": {}, - "Latency: %lldms": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ trễ: %lldms" + "Layout" : { + + }, + "Length" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ dài" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "延迟: %lldms" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "长度" } } } }, - "Layout": {}, - "Length": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ dài" + "Length:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ dài:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "长度" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "长度:" } } } }, - "Length:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ dài:" + "less or equal" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "nhỏ hơn hoặc bằng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "长度:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "小于或等于" } } } }, - "less or equal": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "nhỏ hơn hoặc bằng" + "less than" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "nhỏ hơn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "小于或等于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "小于" } } } }, - "less than": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "nhỏ hơn" + "License" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giấy phép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "小于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "许可证" } } } }, - "License": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giấy phép" + "License expired — sync paused" : { + + }, + "License Key:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mã giấy phép:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "许可证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "许可证密钥:" } } } }, - "License expired — sync paused": {}, - "License Key:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mã giấy phép:" + "Light" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sáng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "许可证密钥:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "浅色" } } } }, - "Light": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sáng" + "Limit" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giới hạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "浅色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "限制" } } } }, - "Limit": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giới hạn" + "Line break" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuống dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "限制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "换行" } } } }, - "Line break": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xuống dòng" + "Line Number" : { + + }, + "Load" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "换行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载" } } } }, - "Line Number": {}, - "Load": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải" + "Load in Editor" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải vào trình soạn thảo" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在编辑器中加载" } } } }, - "Load in Editor": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải vào trình soạn thảo" + "Load Table Template" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải mẫu bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在编辑器中加载" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载表模板" } } } }, - "Load Table Template": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải mẫu bảng" + "Load Template" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải mẫu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载表模板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载模板" } } } }, - "Load Template": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải mẫu" + "Loading databases..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang tải cơ sở dữ liệu..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载模板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载数据库..." } } } }, - "Loading databases...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang tải cơ sở dữ liệu..." + "Loading plugins..." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang tải plugin..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在加载数据库..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载插件..." } } } }, - "Loading plugins...": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang tải plugin..." + "Loading schemas..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang tải schema..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在加载插件..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载 Schema..." } } } }, - "Loading schemas...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang tải schema..." + "Loading tables..." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang tải danh sách bảng..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在加载 Schema..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载表..." } } } }, - "Loading tables...": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang tải danh sách bảng..." + "Loading..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang tải..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在加载表..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "加载中..." } } } }, - "Loading...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang tải..." + "localhost" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "localhost" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "加载中..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "localhost" } } } }, - "localhost": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "localhost" + "Maintenance" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảo trì" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "localhost" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "维护" } } } }, - "Maintenance": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảo trì" + "Majority" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Majority" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "维护" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Majority" } } } }, - "Majority": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Majority" + "Manage Connections..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quản lý kết nối..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Majority" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理连接..." } } } }, - "Manage Connections...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Quản lý kết nối..." + "Manage Tags" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quản lý thẻ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "管理连接..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理标签" } } } }, - "Manage Tags": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Quản lý thẻ" + "Manual" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thủ công" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "管理标签" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手动" } } } }, - "Manual": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thủ công" + "Massive" : { + + }, + "Match ALL filters (AND) or ANY filter (OR)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khớp TẤT CẢ bộ lọc (AND) hoặc BẤT KỲ bộ lọc (OR)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "手动" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "匹配所有筛选条件 (AND) 或任意筛选条件 (OR)" } } } }, - "Massive": {}, - "Match ALL filters (AND) or ANY filter (OR)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khớp TẤT CẢ bộ lọc (AND) hoặc BẤT KỲ bộ lọc (OR)" + "matches regex" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "khớp regex" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "匹配所有筛选条件 (AND) 或任意筛选条件 (OR)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "匹配正则" } } } }, - "matches regex": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "khớp regex" + "Max %lld characters" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tối đa %lld ký tự" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "匹配正则" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最多 %lld 个字符" } } } }, - "Max %lld characters": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tối đa %lld ký tự" + "Max schema tables: %lld" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số bảng tối đa: %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "最多 %lld 个字符" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema 最大表数:%lld" } } } }, - "Max schema tables: %lld": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số bảng tối đa: %lld" + "Maximum days cannot be negative" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số ngày tối đa không được là số âm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Schema 最大表数:%lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大天数不能为负数" } } } }, - "Maximum days cannot be negative": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số ngày tối đa không được là số âm" + "Maximum entries cannot be negative" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số mục tối đa không được là số âm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "最大天数不能为负数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大条目数不能为负数" } } } }, - "Maximum entries cannot be negative": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số mục tối đa không được là số âm" + "Maximum entries:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số mục tối đa:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "最大条目数不能为负数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最大条目数:" } } } }, - "Maximum entries:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số mục tối đa:" + "Maximum time to wait for a query to complete. Set to 0 for no limit. Applied to new connections." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thời gian chờ tối đa để truy vấn hoàn thành. Đặt 0 để không giới hạn. Áp dụng cho kết nối mới." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "最大条目数:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "等待查询完成的最长时间。设为 0 表示不限制。应用于新连接。" } } } }, - "Maximum time to wait for a query to complete. Set to 0 for no limit. Applied to new connections.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thời gian chờ tối đa để truy vấn hoàn thành. Đặt 0 để không giới hạn. Áp dụng cho kết nối mới." + "Medium" : { + + }, + "METADATA" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SIÊU DỮ LIỆU" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "等待查询完成的最长时间。设为 0 表示不限制。应用于新连接。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "元数据" } } } }, - "Medium": {}, - "METADATA": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SIÊU DỮ LIỆU" + "Method" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phương thức" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "元数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "方法" } } } }, - "Method": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phương thức" + "Missing argument: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thiếu đối số: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "方法" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "缺少参数:%@" } } } }, - "Missing argument: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thiếu đối số: %@" + "Model" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Model" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "缺少参数:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Model" } } } }, - "Model": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Model" + "Model not found: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy model: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Model" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到模型:%@" } } } }, - "Model not found: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy model: %@" + "Modified" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã sửa đổi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到模型:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已修改" } } } }, - "Modified": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã sửa đổi" + "Modified:" : { + + }, + "MongoDB" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "MongoDB" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已修改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "MongoDB" } } } }, - "Modified:": {}, - "MongoDB": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "MongoDB" + "Move Down" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Di chuyển xuống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "MongoDB" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下移" } } } }, - "Move Down": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Di chuyển xuống" + "Move Down (⌘↓)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Di chuyển xuống (⌘↓)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下移" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下移 (⌘↓)" } } } }, - "Move Down (⌘↓)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Di chuyển xuống (⌘↓)" + "Move to" : { + + }, + "Move Up" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Di chuyển lên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下移 (⌘↓)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上移" } } } }, - "Move to": {}, - "Move Up": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Di chuyển lên" + "Move Up (⌘↑)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Di chuyển lên (⌘↑)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上移" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上移 (⌘↑)" } } } }, - "Move Up (⌘↑)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Di chuyển lên (⌘↑)" + "MQL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上移 (⌘↑)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQL" } } } }, - "MQL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "MQL" + "MQL Preview" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước MQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "MQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "MQL 预览" } } } }, - "MQL Preview": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước MQL" + "Multiple values" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhiều giá trị" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "MQL 预览" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "多个值" } } } }, - "Multiple values": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhiều giá trị" + "Name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "多个值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "名称" } } } }, - "Name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên" + "Name:" : { + + }, + "Navigate to referenced row" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đi đến dòng được tham chiếu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跳转到引用行" } } } }, - "Name:": {}, - "Navigate to referenced row": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đi đến dòng được tham chiếu" + "Navigating pages will reload data and discard all unsaved changes." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển trang sẽ tải lại dữ liệu và hủy tất cả thay đổi chưa lưu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "跳转到引用行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "翻页将重新加载数据并丢弃所有未保存的更改。" } } } }, - "Navigating to another page will discard all unsaved changes.": {}, - "Nearest": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nearest" + "Navigating to another page will discard all unsaved changes." : { + + }, + "Nearest" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nearest" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Nearest" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nearest" } } } }, - "Network error: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi mạng: %@" + "Network error: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi mạng: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "网络错误:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "网络错误:%@" } } } }, - "Network is unavailable. Changes will sync when connectivity is restored.": {}, - "Never": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không bao giờ" + "Network is unavailable. Changes will sync when connectivity is restored." : { + + }, + "Never" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không bao giờ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从不" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从不" } } } }, - "New Chat": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cuộc trò chuyện mới" + "New Chat" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuộc trò chuyện mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建对话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建对话" } } } }, - "New Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối mới" + "New Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建连接" } } } }, - "New Connection (⌘N)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối mới (⌘N)" + "New Connection (⌘N)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối mới (⌘N)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建连接 (⌘N)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建连接 (⌘N)" } } } }, - "New Connection...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối mới..." + "New Connection..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối mới..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建连接..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建连接..." } } } }, - "New Conversation": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hội thoại mới" + "New Conversation" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hội thoại mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建会话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建会话" } } } }, - "New Favorite": {}, - "New Favorite...": {}, - "New Folder": {}, - "New Group": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhóm mới" + "New Favorite" : { + + }, + "New Favorite..." : { + + }, + "New Folder" : { + + }, + "New Group" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhóm mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建分组" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建分组" } } } }, - "New Jump Host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Jump Host mới" + "New Jump Host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jump Host mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建 Jump Host" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建 Jump Host" } } } }, - "New query tab": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab truy vấn mới" + "New query tab" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab truy vấn mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建查询标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建查询标签页" } } } }, - "New Query Tab": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab truy vấn mới" + "New Query Tab" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab truy vấn mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建查询标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建查询标签页" } } } }, - "New Query Tab (⌘T)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab truy vấn mới (⌘T)" + "New Query Tab (⌘T)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab truy vấn mới (⌘T)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建查询标签页 (⌘T)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建查询标签页 (⌘T)" } } } }, - "New Subfolder": {}, - "New Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab mới" + "New Subfolder" : { + + }, + "New Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建标签页" } } } }, - "New Theme": {}, - "New View...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "View mới..." + "New Theme" : { + + }, + "New View..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "View mới..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "新建视图..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建视图..." } } } }, - "Next Page (⌘])": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trang sau (⌘])" + "Next Page (⌘])" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trang sau (⌘])" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下一页 (⌘])" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一页 (⌘])" } } } }, - "Next Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab tiếp" + "Next Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab tiếp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下一个标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一个标签页" } } } }, - "Next Tab (Alt)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab tiếp theo (Alt)" + "Next Tab (Alt)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab tiếp theo (Alt)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下一个标签页 (Alt)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一个标签页 (Alt)" } } } }, - "No %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có %@" + "No %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有%@" } } } }, - "No active connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có kết nối đang hoạt động" + "No active connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có kết nối đang hoạt động" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无活动连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无活动连接" } } } }, - "No AI provider configured. Go to Settings > AI to add one.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa cấu hình nhà cung cấp AI. Vào Cài đặt > AI để thêm." + "No AI provider configured. Go to Settings > AI to add one." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa cấu hình nhà cung cấp AI. Vào Cài đặt > AI để thêm." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尚未配置 AI 提供商。前往设置 > AI 添加。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未配置 AI 提供商。前往设置 > AI 添加。" } } } }, - "No available local port for SSH tunnel": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có cổng nội bộ khả dụng cho đường hầm SSH" + "No available local port for SSH tunnel" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có cổng nội bộ khả dụng cho đường hầm SSH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有可用的本地端口用于 SSH 隧道" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有可用的本地端口用于 SSH 隧道" } } } }, - "No changes to preview": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có thay đổi để xem trước" + "No changes to preview" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có thay đổi để xem trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无可预览的更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无可预览的更改" } } } }, - "No Check Constraints": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có ràng buộc kiểm tra" + "No Check Constraints" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có ràng buộc kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无检查约束" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无检查约束" } } } }, - "No Columns Defined": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có cột nào" + "No Columns Defined" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có cột nào" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尚未定义列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未定义列" } } } }, - "No Connections": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có kết nối" + "No Connections" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无连接" } } } }, - "No connections yet": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có kết nối nào" + "No connections yet" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có kết nối nào" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "暂无连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "暂无连接" } } } }, - "No database connection": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa kết nối cơ sở dữ liệu" + "No database connection" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa kết nối cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未连接数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未连接数据库" } } } }, - "No databases found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy cơ sở dữ liệu" + "No databases found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到数据库" } } } }, - "No databases match \"%@\"": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có cơ sở dữ liệu nào khớp \"%@\"" + "No databases match \"%@\"" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có cơ sở dữ liệu nào khớp \"%@\"" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有匹配 \"%@\" 的数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有匹配 \"%@\" 的数据库" } } } }, - "No DDL available": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có DDL" + "No DDL available" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có DDL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无可用 DDL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无可用 DDL" } } } }, - "No export formats available. Enable export plugins in Settings > Plugins.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có định dạng xuất khả dụng. Bật plugin xuất trong Cài đặt > Plugin." + "No export formats available. Enable export plugins in Settings > Plugins." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có định dạng xuất khả dụng. Bật plugin xuất trong Cài đặt > Plugin." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无可用的导出格式。请在设置 > 插件中启用导出插件。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无可用的导出格式。请在设置 > 插件中启用导出插件。" } } } }, - "No Favorites": {}, - "No Foreign Keys Yet": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có khóa ngoại" + "No Favorites" : { + + }, + "No Foreign Keys Yet" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có khóa ngoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尚无外键" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚无外键" } } } }, - "No iCloud": {}, - "No Indexes Defined": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có chỉ mục" + "No iCloud" : { + + }, + "No Indexes Defined" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có chỉ mục" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尚未定义索引" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未定义索引" } } } }, - "No limit": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không giới hạn" + "No limit" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không giới hạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不限制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不限制" } } } }, - "No matching %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy %@ phù hợp" + "No matching %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy %@ phù hợp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有匹配的%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有匹配的%@" } } } }, - "No matching connections": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có kết nối nào khớp" + "No matching connections" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có kết nối nào khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配连接" } } } }, - "No Matching Connections": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có kết nối phù hợp" + "No Matching Connections" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có kết nối phù hợp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配连接" } } } }, - "No matching databases": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có cơ sở dữ liệu nào khớp" + "No matching databases" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có cơ sở dữ liệu nào khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配数据库" } } } }, - "No Matching Favorites": {}, - "No matching fields": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có trường khớp" + "No Matching Favorites" : { + + }, + "No matching fields" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có trường khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配字段" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配字段" } } } }, - "No matching objects": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy đối tượng phù hợp" + "No matching objects" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy đối tượng phù hợp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有匹配的对象" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有匹配的对象" } } } }, - "No Matching Queries": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có truy vấn phù hợp" + "No Matching Queries" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có truy vấn phù hợp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配查询" } } } }, - "No matching schemas": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có schema khớp" + "No matching schemas" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có schema khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配 Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配 Schema" } } } }, - "No matching tables": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có bảng nào khớp" + "No matching tables" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có bảng nào khớp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无匹配表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无匹配表" } } } }, - "No model selected": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa chọn model" + "No model selected" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa chọn model" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未选择模型" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未选择模型" } } } }, - "No models loaded": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa tải mô hình nào" + "No models loaded" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa tải mô hình nào" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未加载模型" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未加载模型" } } } }, - "No objects found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy đối tượng" + "No objects found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy đối tượng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到对象" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到对象" } } } }, - "No objects match \"%@\"": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có đối tượng phù hợp với \"%@\"" + "No objects match \"%@\"" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có đối tượng phù hợp với \"%@\"" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有匹配\"%@\"的对象" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有匹配\"%@\"的对象" } } } }, - "No parts found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy phân vùng" + "No parts found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy phân vùng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到分区" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到分区" } } } }, - "No pending changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có thay đổi nào" + "No pending changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có thay đổi nào" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无待处理的更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无待处理的更改" } } } }, - "No plugins found": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy plugin" + "No plugins found" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到插件" } } } }, - "No primary key selected (not recommended)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa chọn khóa chính (không khuyến nghị)" + "No primary key selected (not recommended)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa chọn khóa chính (không khuyến nghị)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未选择主键(不推荐)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未选择主键(不推荐)" } } } }, - "No providers configured": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa cấu hình nhà cung cấp" + "No providers configured" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa cấu hình nhà cung cấp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未配置提供商" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未配置提供商" } } } }, - "No query executed yet": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa thực hiện truy vấn nào" + "No query executed yet" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa thực hiện truy vấn nào" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尚未执行查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尚未执行查询" } } } }, - "No Query History Yet": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có lịch sử truy vấn" + "No Query History Yet" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có lịch sử truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "暂无查询历史" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "暂无查询历史" } } } }, - "No rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có dòng" + "No rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无行" } } } }, - "No saved connection named \"%@\".": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có kết nối đã lưu tên \"%@\"." + "No saved connection named \"%@\"." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có kết nối đã lưu tên \"%@\"." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有名为 \"%@\" 的已保存连接。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有名为 \"%@\" 的已保存连接。" } } } }, - "No saved templates": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có mẫu đã lưu" + "No saved templates" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có mẫu đã lưu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无已保存的模板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无已保存的模板" } } } }, - "No schemas found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy schema" + "No schemas found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy schema" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到 Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到 Schema" } } } }, - "No schemas match \"%@\"": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có schema khớp \"%@\"" + "No schemas match \"%@\"" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có schema khớp \"%@\"" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有匹配 \"%@\" 的 Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有匹配 \"%@\" 的 Schema" } } } }, - "No selection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa chọn" + "No selection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa chọn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未选择" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未选择" } } } }, - "No Selection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa chọn" + "No Selection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa chọn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未选择" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未选择" } } } }, - "No SSL encryption": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không mã hóa SSL" + "No SSL encryption" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không mã hóa SSL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无 SSL 加密" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无 SSL 加密" } } } }, - "No Tables": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có bảng" + "No Tables" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无表" } } } }, - "No tables selected for export": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có bảng nào được chọn để xuất" + "No tables selected for export" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có bảng nào được chọn để xuất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未选择要导出的表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未选择要导出的表" } } } }, - "No tabs open": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có tab nào mở" + "No tabs open" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có tab nào mở" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "没有打开的标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有打开的标签页" } } } }, - "No valid rows found in clipboard data.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy dòng hợp lệ trong dữ liệu bộ nhớ tạm." + "No valid rows found in clipboard data." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy dòng hợp lệ trong dữ liệu bộ nhớ tạm." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "剪贴板数据中未找到有效行。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "剪贴板数据中未找到有效行。" } } } }, - "No values found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy giá trị" + "No values found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy giá trị" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到值" } } } }, - "None": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không" + "None" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无" } } } }, - "Normal": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bình thường" + "Normal" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bình thường" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正常" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正常" } } } }, - "Not Available": {}, - "Not connected": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa kết nối" + "Not Available" : { + + }, + "Not connected" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未连接" } } } }, - "Not connected to ClickHouse": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa kết nối đến ClickHouse" + "Not connected to ClickHouse" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa kết nối đến ClickHouse" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未连接到 ClickHouse" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未连接到 ClickHouse" } } } }, - "Not connected to database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa kết nối cơ sở dữ liệu" + "Not connected to database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa kết nối cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未连接到数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未连接到数据库" } } } }, - "not contains": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "không chứa" + "not contains" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "không chứa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不包含" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不包含" } } } }, - "not equals": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "không bằng" + "not equals" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "không bằng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不等于" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不等于" } } } }, - "not in list": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "không trong danh sách" + "not in list" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "không trong danh sách" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不在列表中" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不在列表中" } } } }, - "NOT NULL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "NOT NULL" + "NOT NULL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "NOT NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NOT NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NOT NULL" } } } }, - "Not supported for this database.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không hỗ trợ cho cơ sở dữ liệu này." + "Not supported for this database." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hỗ trợ cho cơ sở dữ liệu này." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此数据库不支持。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此数据库不支持。" } } } }, - "Not supported for this database. Use CASCADE instead.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không hỗ trợ cho cơ sở dữ liệu này. Hãy dùng CASCADE thay thế." + "Not supported for this database. Use CASCADE instead." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hỗ trợ cho cơ sở dữ liệu này. Hãy dùng CASCADE thay thế." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此数据库不支持。请使用 CASCADE 代替。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此数据库不支持。请使用 CASCADE 代替。" } } } }, - "Not supported for TRUNCATE with this database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không hỗ trợ TRUNCATE với cơ sở dữ liệu này" + "Not supported for TRUNCATE with this database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không hỗ trợ TRUNCATE với cơ sở dữ liệu này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此数据库不支持 TRUNCATE" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此数据库不支持 TRUNCATE" } } } }, - "NOW()": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "NOW()" + "NOW()" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "NOW()" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NOW()" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NOW()" } } } }, - "NULL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "NULL" + "NULL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NULL" } } } }, - "NULL display cannot be empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị NULL không được để trống" + "NULL display cannot be empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị NULL không được để trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NULL 显示不能为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NULL 显示不能为空" } } } }, - "NULL display contains invalid characters (newlines/tabs)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị NULL chứa ký tự không hợp lệ (xuống dòng/tab)" + "NULL display contains invalid characters (newlines/tabs)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị NULL chứa ký tự không hợp lệ (xuống dòng/tab)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NULL 显示包含无效字符(换行符/制表符)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NULL 显示包含无效字符(换行符/制表符)" } } } }, - "NULL display must be %lld characters or less": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị NULL phải có %lld ký tự trở xuống" + "NULL display must be %lld characters or less" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị NULL phải có %lld ký tự trở xuống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NULL 显示不能超过 %lld 个字符" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NULL 显示不能超过 %lld 个字符" } } } }, - "NULL display:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị NULL:" + "NULL display:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị NULL:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "NULL 显示:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "NULL 显示:" } } } }, - "NULL Value": {}, - "Nullable": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cho phép NULL" + "NULL Value" : { + + }, + "Nullable" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cho phép NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "允许 NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "允许 NULL" } } } }, - "Number": {}, - "Number of documents per insertMany statement. Higher values create fewer statements.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số lượng document cho mỗi câu lệnh insertMany. Giá trị cao hơn tạo ít câu lệnh hơn." + "Number" : { + + }, + "Number of documents per insertMany statement. Higher values create fewer statements." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số lượng document cho mỗi câu lệnh insertMany. Giá trị cao hơn tạo ít câu lệnh hơn." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每个 insertMany 语句的文档数量。值越大生成的语句越少。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每个 insertMany 语句的文档数量。值越大生成的语句越少。" } } } }, - "Offset": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Vị trí bắt đầu" + "Offset" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vị trí bắt đầu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "偏移量" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏移量" } } } }, - "OK": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "OK" + "OK" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "OK" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" } } } }, - "On Delete": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khi xóa" + "On Delete" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khi xóa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "删除时" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除时" } } } }, - "On Update": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khi cập nhật" + "On Update" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khi cập nhật" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "更新时" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新时" } } } }, - "Open": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở" + "Open" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开" } } } }, - "Open %@ Editor": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở trình chỉnh sửa %@" + "Open %@ Editor" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở trình chỉnh sửa %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开 %@ 编辑器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 %@ 编辑器" } } } }, - "Open Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở kết nối" + "Open Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开连接" } } } }, - "Open containing folder": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở thư mục chứa" + "Open containing folder" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở thư mục chứa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开所在文件夹" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开所在文件夹" } } } }, - "Open database": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở cơ sở dữ liệu" + "Open database" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开数据库" } } } }, - "Open Database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở cơ sở dữ liệu" + "Open Database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开数据库" } } } }, - "Open Database (⌘K)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở cơ sở dữ liệu (⌘K)" + "Open Database (⌘K)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở cơ sở dữ liệu (⌘K)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开数据库 (⌘K)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开数据库 (⌘K)" } } } }, - "Open Database...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở cơ sở dữ liệu..." + "Open Database..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở cơ sở dữ liệu..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开数据库..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开数据库..." } } } }, - "Open MQL Editor": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở trình soạn MQL" + "Open MQL Editor" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở trình soạn MQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开 MQL 编辑器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 MQL 编辑器" } } } }, - "Open Redis CLI": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở Redis CLI" + "Open Redis CLI" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở Redis CLI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开 Redis CLI" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 Redis CLI" } } } }, - "Open Schema": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở Schema" + "Open Schema" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở Schema" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开 Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 Schema" } } } }, - "Open SQL Editor": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở trình soạn SQL" + "Open SQL Editor" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở trình soạn SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "打开 SQL 编辑器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 SQL 编辑器" } } } }, - "Operation cancelled by user": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thao tác đã bị người dùng hủy" + "Operation cancelled by user" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thao tác đã bị người dùng hủy" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "操作已被用户取消" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "操作已被用户取消" } } } }, - "Operator": {}, - "Optimize Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tối ưu truy vấn" + "Operator" : { + + }, + "Optimize Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tối ưu truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "优化查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优化查询" } } } }, - "Optimize this SQL query for better performance:\n\n```sql\n%@\n```": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đề xuất tối ưu hóa cho câu truy vấn SQL sau:\n\n```sql\n%@\n```" + "Optimize this SQL query for better performance:\n\n```sql\n%@\n```" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đề xuất tối ưu hóa cho câu truy vấn SQL sau:\n\n```sql\n%@\n```" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "优化以下 SQL 查询以提升性能:\n\n```sql\n%@\n```" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优化以下 SQL 查询以提升性能:\n\n```sql\n%@\n```" } } } }, - "Optimize with AI": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tối ưu với AI" + "Optimize with AI" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tối ưu với AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "AI 优化" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 优化" } } } }, - "Optional description": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mô tả tùy chọn" + "Optional description" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mô tả tùy chọn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "可选描述" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "可选描述" } } } }, - "Options": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tùy chọn" + "Options" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tùy chọn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选项" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选项" } } } }, - "OR": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "OR" + "OR" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "OR" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "OR" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "OR" } } } }, - "Oracle": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Oracle" + "Oracle" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oracle" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Oracle" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oracle" } } } }, - "Orange": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cam" + "Orange" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cam" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "橙色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "橙色" } } } }, - "Other": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khác" + "Other" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khác" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "其他" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "其他" } } } }, - "Other Device": {}, - "Page size must be between %@ and %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Page size must be between %1$@ and %2$@" + "Other Device" : { + + }, + "Page size must be between %@ and %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Page size must be between %1$@ and %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích thước trang phải nằm trong khoảng %1$@ đến %2$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích thước trang phải nằm trong khoảng %1$@ đến %2$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "页面大小必须在 %@ 和 %@ 之间" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "页面大小必须在 %@ 和 %@ 之间" } } } }, - "Pagination": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phân trang" + "Pagination" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phân trang" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分页" } } } }, - "Pagination Settings": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt phân trang" + "Pagination Settings" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt phân trang" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分页设置" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分页设置" } } } }, - "Panel State": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trạng thái bảng điều khiển" + "Panel State" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trạng thái bảng điều khiển" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "面板状态" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "面板状态" } } } }, - "Partition": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phân vùng" + "Partition" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phân vùng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "分区" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分区" } } } }, - "Passphrase": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cụm mật khẩu" + "Passphrase" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cụm mật khẩu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "密码短语" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密码短语" } } } }, - "Password": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mật khẩu" + "Password" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mật khẩu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "密码" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密码" } } } }, - "Password is sent via keyboard-interactive challenge-response.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mật khẩu được gửi qua keyboard-interactive challenge-response." + "Password is sent via keyboard-interactive challenge-response." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mật khẩu được gửi qua keyboard-interactive challenge-response." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "密码通过 keyboard-interactive 质询-响应方式发送。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "密码通过 keyboard-interactive 质询-响应方式发送。" } } } }, - "Paste": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dán" + "Paste" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dán" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "粘贴" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "粘贴" } } } }, - "Paste a connection URL to auto-fill the form fields.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dán URL kết nối để tự động điền các trường trong biểu mẫu." + "Paste a connection URL to auto-fill the form fields." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dán URL kết nối để tự động điền các trường trong biểu mẫu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "粘贴连接 URL 以自动填充表单字段。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "粘贴连接 URL 以自动填充表单字段。" } } } }, - "Paste your CREATE TABLE statement below:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dán câu lệnh CREATE TABLE bên dưới:" + "Paste your CREATE TABLE statement below:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dán câu lệnh CREATE TABLE bên dưới:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在下方粘贴 CREATE TABLE 语句:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在下方粘贴 CREATE TABLE 语句:" } } } }, - "pending delete": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "chờ xóa" + "pending delete" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "chờ xóa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "待删除" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "待删除" } } } }, - "pending truncate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "chờ truncate" + "pending truncate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "chờ truncate" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "待清空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "待清空" } } } }, - "Period": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chu kỳ" + "Period" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chu kỳ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "周期" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "周期" } } } }, - "Pink": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hồng" + "Pink" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hồng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "粉色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "粉色" } } } }, - "Please select a column": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Vui lòng chọn một cột" + "Please select a column" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vui lòng chọn một cột" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "请选择一列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请选择一列" } } } }, - "Plugin '%@' has an invalid descriptor: %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Plugin '%1$@' has an invalid descriptor: %2$@" + "Plugin '%@' has an invalid descriptor: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Plugin '%1$@' has an invalid descriptor: %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin '%@' có mô tả không hợp lệ: %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin '%@' có mô tả không hợp lệ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件 '%@' 的描述符无效:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件 '%@' 的描述符无效:%@" } } } }, - "Plugin checksum does not match expected value": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Checksum plugin không khớp với giá trị mong đợi" + "Plugin checksum does not match expected value" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checksum plugin không khớp với giá trị mong đợi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件校验和与期望值不匹配" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件校验和与期望值不匹配" } } } }, - "Plugin code signature verification failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác minh chữ ký mã plugin thất bại: %@" + "Plugin code signature verification failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác minh chữ ký mã plugin thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件代码签名验证失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件代码签名验证失败:%@" } } } }, - "Plugin does not contain a compatible binary for this architecture": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin không chứa tập tin nhị phân tương thích cho kiến trúc này" + "Plugin does not contain a compatible binary for this architecture" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin không chứa tập tin nhị phân tương thích cho kiến trúc này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件不包含兼容此架构的二进制文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件不包含兼容此架构的二进制文件" } } } }, - "Plugin download failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tải plugin thất bại: %@" + "Plugin download failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tải plugin thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件下载失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件下载失败:%@" } } } }, - "Plugin Installation Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt Plugin thất bại" + "Plugin Installation Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt Plugin thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件安装失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件安装失败" } } } }, - "Plugin installation failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cài đặt plugin thất bại: %@" + "Plugin installation failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cài đặt plugin thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件安装失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件安装失败:%@" } } } }, - "Plugin not found": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy plugin" + "Plugin not found" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到插件" } } } }, - "Plugin Not Installed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin chưa được cài đặt" + "Plugin Not Installed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin chưa được cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件未安装" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件未安装" } } } }, - "Plugin requires app version %@ or later, but current version is %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Plugin requires app version %1$@ or later, but current version is %2$@" + "Plugin requires app version %@ or later, but current version is %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Plugin requires app version %1$@ or later, but current version is %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin yêu cầu ứng dụng phiên bản %@ trở lên, nhưng phiên bản hiện tại là %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin yêu cầu ứng dụng phiên bản %@ trở lên, nhưng phiên bản hiện tại là %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件需要应用版本 %@ 或更高,但当前版本为 %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件需要应用版本 %@ 或更高,但当前版本为 %@" } } } }, - "Plugin requires PluginKit version %lld, but app provides version %lld": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Plugin requires PluginKit version %1$lld, but app provides version %2$lld" + "Plugin requires PluginKit version %lld, but app provides version %lld" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Plugin requires PluginKit version %1$lld, but app provides version %2$lld" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin yêu cầu PluginKit phiên bản %lld, nhưng ứng dụng cung cấp phiên bản %lld" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin yêu cầu PluginKit phiên bản %lld, nhưng ứng dụng cung cấp phiên bản %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件需要 PluginKit 版本 %lld,但应用提供的版本为 %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件需要 PluginKit 版本 %lld,但应用提供的版本为 %lld" } } } }, - "Plugins": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin" + "Plugins" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "插件" } } } }, - "Port": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cổng" + "Port" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cổng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "端口" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "端口" } } } }, - "postgresql://user:password@host:5432/database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "postgresql://user:password@host:5432/database" + "postgresql://user:password@host:5432/database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "postgresql://user:password@host:5432/database" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "postgresql://user:password@host:5432/database" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "postgresql://user:password@host:5432/database" } } } }, - "Potentially Dangerous Queries": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn có thể nguy hiểm" + "Potentially Dangerous Queries" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn có thể nguy hiểm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "潜在危险查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "潜在危险查询" } } } }, - "Potentially Dangerous Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn có thể nguy hiểm" + "Potentially Dangerous Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn có thể nguy hiểm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "潜在危险查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "潜在危险查询" } } } }, - "Pre-Connect Script": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Script pre-connect" + "Pre-Connect Script" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Script pre-connect" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前脚本" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前脚本" } } } }, - "Pre-connect script failed (exit %d): %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Pre-connect script failed (exit %1$d): %2$@" + "Pre-connect script failed (exit %d): %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pre-connect script failed (exit %1$d): %2$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tập lệnh tiền kết nối thất bại (exit %d): %@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tập lệnh tiền kết nối thất bại (exit %d): %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前脚本失败(退出码 %d):%@" + } + } + } + }, + "Pre-connect script failed (exit %lld): %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Script pre-connect thất bại (exit %lld): %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前脚本失败(退出码 %d):%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前脚本失败(退出码 %lld):%@" } } } }, - "Pre-connect script failed (exit %lld): %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Script pre-connect thất bại (exit %lld): %@" + "Pre-connect script failed with exit code %d" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tập lệnh tiền kết nối thất bại với mã thoát %d" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前脚本失败(退出码 %lld):%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前脚本失败,退出码 %d" } } } }, - "Pre-connect script failed with exit code %d": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tập lệnh tiền kết nối thất bại với mã thoát %d" + "Pre-connect script failed with exit code %lld" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Script pre-connect thất bại với exit code %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前脚本失败,退出码 %d" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前脚本失败,退出码 %lld" } } } }, - "Pre-connect script failed with exit code %lld": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Script pre-connect thất bại với exit code %lld" + "Pre-connect script timed out after 10 seconds" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Script pre-connect hết thời gian chờ sau 10 giây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前脚本失败,退出码 %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前脚本超时(10秒后)" } } } }, - "Pre-connect script timed out after 10 seconds": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Script pre-connect hết thời gian chờ sau 10 giây" + "Precision" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ chính xác" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前脚本超时(10秒后)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "精度" } } } }, - "Precision": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ chính xác" + "Precision:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ chính xác:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "精度" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "精度:" } } } }, - "Precision:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ chính xác:" + "Preserve all values as strings" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giữ nguyên tất cả giá trị dưới dạng chuỗi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "精度:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将所有值保留为字符串" } } } }, - "Preserve all values as strings": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giữ nguyên tất cả giá trị dưới dạng chuỗi" + "Preset Name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên mẫu đặt trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将所有值保留为字符串" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预设名称" } } } }, - "Preset Name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên mẫu đặt trước" + "Pretty Print" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Định dạng đẹp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预设名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式化输出" } } } }, - "Pretty Print": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Định dạng đẹp" + "Pretty print (formatted output)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "In đẹp (đầu ra có định dạng)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "格式化输出" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "格式化输出(带格式)" } } } }, - "Pretty print (formatted output)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "In đẹp (đầu ra có định dạng)" + "Prevent CSV formula injection by prefixing values starting with =, +, -, @ with a single quote" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngăn chặn chèn công thức CSV bằng cách thêm dấu nháy đơn trước các giá trị bắt đầu bằng =, +, -, @" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "格式化输出(带格式)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通过在以 =、+、-、@ 开头的值前添加单引号来防止 CSV 公式注入" } } } }, - "Prevent CSV formula injection by prefixing values starting with =, +, -, @ with a single quote": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ngăn chặn chèn công thức CSV bằng cách thêm dấu nháy đơn trước các giá trị bắt đầu bằng =, +, -, @" + "Prevent write operations (INSERT, UPDATE, DELETE, DROP, etc.)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngăn chặn thao tác ghi (INSERT, UPDATE, DELETE, DROP, v.v.)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "通过在以 =、+、-、@ 开头的值前添加单引号来防止 CSV 公式注入" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "禁止写操作(INSERT、UPDATE、DELETE、DROP 等)" } } } }, - "Prevent write operations (INSERT, UPDATE, DELETE, DROP, etc.)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ngăn chặn thao tác ghi (INSERT, UPDATE, DELETE, DROP, v.v.)" + "Preview" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "禁止写操作(INSERT、UPDATE、DELETE、DROP 等)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览" } } } }, - "Preview": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước" + "Preview %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览 %@" } } } }, - "Preview %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước %@" + "Preview %@ (⌘⇧P)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước %@ (⌘⇧P)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览 %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览 %@ (⌘⇧P)" } } } }, - "Preview %@ (⌘⇧P)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước %@ (⌘⇧P)" + "Preview Commands" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览 %@ (⌘⇧P)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览命令" } } } }, - "Preview Commands": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước lệnh" + "Preview Commands (⌘⇧P)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước lệnh (⌘⇧P)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览命令" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览命令 (⌘⇧P)" } } } }, - "Preview Commands (⌘⇧P)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước lệnh (⌘⇧P)" + "Preview MQL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước MQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览命令 (⌘⇧P)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览 MQL" } } } }, - "Preview MQL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước MQL" + "Preview MQL (⌘⇧P)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước MQL (⌘⇧P)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览 MQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览 MQL (⌘⇧P)" } } } }, - "Preview MQL (⌘⇧P)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước MQL (⌘⇧P)" + "Preview Schema Changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước thay đổi cấu trúc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览 MQL (⌘⇧P)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览结构更改" } } } }, - "Preview Schema Changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước thay đổi cấu trúc" + "Preview SQL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览结构更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览 SQL" } } } }, - "Preview SQL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước SQL" + "Preview SQL (⌘⇧P)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước SQL (⌘⇧P)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览 SQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预览 SQL (⌘⇧P)" } } } }, - "Preview SQL (⌘⇧P)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước SQL (⌘⇧P)" + "Previous Page (⌘[)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trang trước (⌘[)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "预览 SQL (⌘⇧P)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一页 (⌘[)" } } } }, - "Previous Page (⌘[)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trang trước (⌘[)" + "Previous Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上一页 (⌘[)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一个标签页" } } } }, - "Previous Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab trước" + "Previous Tab (Alt)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab trước (Alt)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上一个标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一个标签页 (Alt)" } } } }, - "Previous Tab (Alt)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab trước (Alt)" + "Primary" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primary" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上一个标签页 (Alt)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primary" } } } }, - "Primary": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Primary" + "Primary Key" : { + + }, + "Primary Preferred" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primary Preferred" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Primary" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Primary Preferred" } } } }, - "Primary Key": {}, - "Primary Preferred": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Primary Preferred" + "Primary Text" : { + + }, + "Privacy" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quyền riêng tư" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Primary Preferred" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐私" } } } }, - "Primary Text": {}, - "Privacy": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Quyền riêng tư" + "Private Key" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khóa riêng tư" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "隐私" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "私钥" } } } }, - "Private Key": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khóa riêng tư" + "Pro license required for iCloud Sync" : { + + }, + "Prompt at Connect" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhắc khi kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "私钥" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接时提示" } } } }, - "Pro license required for iCloud Sync": {}, - "Prompt at Connect": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhắc khi kết nối" + "Providers" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhà cung cấp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接时提示" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提供商" } } } }, - "Providers": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhà cung cấp" + "Public key authentication failed: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực khóa công khai thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "提供商" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "公钥认证失败:%@" } } } }, - "Purple": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tím" + "Purple" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tím" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "紫色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "紫色" } } } }, - "Put field names in the first row": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt tên trường ở dòng đầu tiên" + "Put field names in the first row" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt tên trường ở dòng đầu tiên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "将字段名放在第一行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将字段名放在第一行" } } } }, - "Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn" + "Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询" } } } }, - "Query cancelled": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã hủy truy vấn" + "Query cancelled" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã hủy truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "查询已取消" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询已取消" } } } }, - "Query executed successfully": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn thực thi thành công" + "Query executed successfully" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn thực thi thành công" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "查询执行成功" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询执行成功" } } } }, - "Query executing": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang thực hiện truy vấn" + "Query executing" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang thực hiện truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在执行查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在执行查询" } } } }, - "Query executing...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang thực thi truy vấn..." + "Query executing..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang thực thi truy vấn..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在执行查询..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在执行查询..." } } } }, - "Query Execution": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thực thi truy vấn" + "Query Execution" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thực thi truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "查询执行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询执行" } } } }, - "Query Execution Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thực thi truy vấn thất bại" + "Query Execution Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thực thi truy vấn thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "查询执行失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询执行失败" } } } }, - "Query History:": {}, - "Query timeout:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thời gian chờ truy vấn:" + "Query History:" : { + + }, + "Query timeout:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thời gian chờ truy vấn:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "查询超时:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询超时:" } } } }, - "Query:": {}, - "Quick search across all columns...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm kiếm nhanh trên tất cả các cột..." + "Query:" : { + + }, + "Quick search across all columns..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm kiếm nhanh trên tất cả các cột..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快速搜索所有列..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快速搜索所有列..." } } } }, - "Quick Switcher": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển đổi nhanh" + "Quick Switcher" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển đổi nhanh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快速切换" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快速切换" } } } }, - "Quick Switcher (⌘P)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển đổi nhanh (⌘P)" + "Quick Switcher (⌘P)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển đổi nhanh (⌘P)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快速切换 (⌘P)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快速切换 (⌘P)" } } } }, - "Quick Switcher...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển đổi nhanh..." + "Quick Switcher..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển đổi nhanh..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快速切换..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快速切换..." } } } }, - "Quote": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dấu ngoặc kép" + "Quote" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dấu ngoặc kép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "引号" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "引号" } } } }, - "Quote if needed": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt trong ngoặc kép nếu cần" + "Quote if needed" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt trong ngoặc kép nếu cần" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "按需加引号" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按需加引号" } } } }, - "Rate limited. Please try again later.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã vượt giới hạn tốc độ. Vui lòng thử lại sau." + "Rate limited. Please try again later." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã vượt giới hạn tốc độ. Vui lòng thử lại sau." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "请求频率超限。请稍后再试。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请求频率超限。请稍后再试。" } } } }, - "Raw SQL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SQL thô" + "Raw SQL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL thô" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "原始 SQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "原始 SQL" } } } }, - "Raw SQL cannot be empty": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SQL thô không được để trống" + "Raw SQL cannot be empty" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL thô không được để trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "原始 SQL 不能为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "原始 SQL 不能为空" } } } }, - "Read Preference": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Read Preference" + "Read Preference" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Read Preference" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Read Preference" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Read Preference" } } } }, - "Read-only": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chỉ đọc" + "Read-only" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chỉ đọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "只读" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "只读" } } } }, - "Read-Only": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chỉ đọc" + "Read-Only" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chỉ đọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "只读" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "只读" } } } }, - "Read-only connection": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối chỉ đọc" + "Read-only connection" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối chỉ đọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "只读连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "只读连接" } } } }, - "Reassign": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gán lại" + "Reassign" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gán lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新分配" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新分配" } } } }, - "RECENT": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "GẦN ĐÂY" + "RECENT" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "GẦN ĐÂY" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "最近" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最近" } } } }, - "Recent Conversations": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cuộc trò chuyện gần đây" + "Recent Conversations" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cuộc trò chuyện gần đây" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "最近的会话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最近的会话" } } } }, - "Reconnect": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối lại" + "Reconnect" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新连接" } } } }, - "Reconnect failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối lại thất bại: %@" + "Reconnect failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối lại thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新连接失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新连接失败:%@" } } } }, - "Reconnect to Database": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối lại cơ sở dữ liệu" + "Reconnect to Database" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối lại cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新连接数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新连接数据库" } } } }, - "Recording shortcut": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đang ghi phím tắt" + "Recording shortcut" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đang ghi phím tắt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在录制快捷键" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在录制快捷键" } } } }, - "Red": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đỏ" + "Red" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đỏ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "红色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "红色" } } } }, - "Redis": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Redis" + "Redis" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redis" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Redis" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Redis" } } } }, - "Redo": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm lại" + "Redo" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重做" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重做" } } } }, - "Ref Columns": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột tham chiếu" + "Ref Columns" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột tham chiếu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "引用列" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用列" } } } }, - "Ref Table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng tham chiếu" + "Ref Table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng tham chiếu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "引用表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用表" } } } }, - "Referenced columns (comma-separated)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cột tham chiếu (phân tách bằng dấu phẩy)" + "Referenced columns (comma-separated)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cột tham chiếu (phân tách bằng dấu phẩy)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "引用列(逗号分隔)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用列(逗号分隔)" } } } }, - "Referenced table": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng tham chiếu" + "Referenced table" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng tham chiếu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "引用表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "引用表" } } } }, - "Refresh": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm mới" + "Refresh" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "刷新" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新" } } } }, - "Refresh (⌘R)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm mới (⌘R)" + "Refresh (⌘R)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm mới (⌘R)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "刷新 (⌘R)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新 (⌘R)" } } } }, - "Refresh data": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm mới dữ liệu" + "Refresh data" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm mới dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "刷新数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新数据" } } } }, - "Refresh database list": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm mới danh sách cơ sở dữ liệu" + "Refresh database list" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm mới danh sách cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "刷新数据库列表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新数据库列表" } } } }, - "Refreshing will discard all unsaved changes.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm mới sẽ hủy tất cả thay đổi chưa lưu." + "Refreshing will discard all unsaved changes." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm mới sẽ hủy tất cả thay đổi chưa lưu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "刷新将丢弃所有未保存的更改。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新将丢弃所有未保存的更改。" } } } }, - "Regenerate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo lại" + "Regenerate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新生成" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新生成" } } } }, - "Registry": {}, - "Remove filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa bộ lọc" + "Registry" : { + + }, + "Remove filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "移除筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除筛选" } } } }, - "Remove Filter": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa bộ lọc" + "Remove Filter" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "移除筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移除筛选" } } } }, - "Remove license from this machine": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gỡ giấy phép khỏi máy này" + "Remove license from this machine" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gỡ giấy phép khỏi máy này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "从此设备移除许可证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从此设备移除许可证" } } } }, - "Rename": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đổi tên" + "Rename" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đổi tên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重命名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重命名" } } } }, - "Rename Group": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đổi tên nhóm" + "Rename Group" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đổi tên nhóm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重命名分组" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重命名分组" } } } }, - "Renew License...": {}, - "Reopen Last Session": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mở lại phiên làm việc trước" + "Renew License..." : { + + }, + "Reopen Last Session" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở lại phiên làm việc trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新打开上次会话" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新打开上次会话" } } } }, - "Replication lag: %llds": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ trễ sao chép: %llds" + "Replication lag: %llds" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ trễ sao chép: %llds" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复制延迟:%llds" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制延迟:%llds" } } } }, - "Require SSL, skip verification": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Yêu cầu SSL, bỏ qua xác minh" + "Require SSL, skip verification" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yêu cầu SSL, bỏ qua xác minh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "要求 SSL,跳过验证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "要求 SSL,跳过验证" } } } }, - "Requires": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Yêu cầu" + "Required (skip verify)" : { + + }, + "Requires" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yêu cầu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "需要" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要" } } } }, - "Reset to Defaults": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khôi phục mặc định" + "Reset to Defaults" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khôi phục mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "恢复默认" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "恢复默认" } } } }, - "Restart TablePro for the language change to take full effect.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khởi động lại TablePro để thay đổi ngôn ngữ có hiệu lực hoàn toàn." + "Restart TablePro for the language change to take full effect." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khởi động lại TablePro để thay đổi ngôn ngữ có hiệu lực hoàn toàn." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重启 TablePro 以使语言更改完全生效。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重启 TablePro 以使语言更改完全生效。" } } } }, - "Restart TablePro to fully unload removed plugins.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khởi động lại TablePro để gỡ hoàn toàn các plugin đã xoá." + "Restart TablePro to fully unload removed plugins." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khởi động lại TablePro để gỡ hoàn toàn các plugin đã xoá." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重启 TablePro 以完全卸载已移除的插件。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重启 TablePro 以完全卸载已移除的插件。" } } } }, - "Retention": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu giữ" + "Retention" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu giữ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保留" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留" } } } }, - "Retry": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thử lại" + "Retry" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重试" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" } } } }, - "Retry Install": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thử cài đặt lại" + "Retry Install" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử cài đặt lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重试安装" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试安装" } } } }, - "Reuse clean table tab": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tái sử dụng tab bảng trống" + "Reuse clean table tab" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tái sử dụng tab bảng trống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "复用空白表标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复用空白表标签页" } } } }, - "Right-click to show all %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấp chuột phải để hiển thị tất cả %@" + "Right-click to show all %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấp chuột phải để hiển thị tất cả %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "右键点击显示全部%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "右键点击显示全部%@" } } } }, - "Right-click to show all tables": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấn chuột phải để hiện tất cả bảng" + "Right-click to show all tables" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấn chuột phải để hiện tất cả bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "右键单击显示所有表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "右键单击显示所有表" } } } }, - "root": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "root" + "root" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "root" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "root" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "root" } } } }, - "Root Level": {}, - "Row %lld": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hàng %lld" + "Root Level" : { + + }, + "Row %lld" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hàng %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行 %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行 %lld" } } } }, - "Row %lld, column %lld: %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Row %1$lld, column %2$lld: %3$@" + "Row %lld, column %lld: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Row %1$lld, column %2$lld: %3$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hàng %lld, cột %lld: %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hàng %lld, cột %lld: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行 %lld,列 %lld:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行 %lld,列 %lld:%@" } } } }, - "Row Details": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chi tiết dòng" + "Row Details" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chi tiết dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行详情" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行详情" } } } }, - "Row height:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chiều cao dòng:" + "Row height:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiều cao dòng:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行高:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行高:" } } } }, - "Row Heights": {}, - "Row number": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số hàng" + "Row Heights" : { + + }, + "Row number" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số hàng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行号" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行号" } } } }, - "Row Number": {}, - "Rows": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dòng" + "Row Number" : { + + }, + "Rows" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "行" } } } }, - "Rows per INSERT": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dòng mỗi INSERT" + "Rows per INSERT" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dòng mỗi INSERT" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每条 INSERT 的行数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每条 INSERT 的行数" } } } }, - "Rows per insertMany": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Số dòng mỗi insertMany" + "Rows per insertMany" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số dòng mỗi insertMany" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每条 insertMany 的行数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每条 insertMany 的行数" } } } }, - "Run a query to see execution time": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chạy truy vấn để xem thời gian thực thi" + "Run a query to see execution time" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chạy truy vấn để xem thời gian thực thi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "运行查询以查看执行时间" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "运行查询以查看执行时间" } } } }, - "Run in New Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chạy trong tab mới" + "Run in New Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chạy trong tab mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在新标签页中运行" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在新标签页中运行" } } } }, - "Safe Mode": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ an toàn" + "Safe Mode" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ an toàn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安全模式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安全模式" } } } }, - "Safe Mode (Full)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ an toàn (Đầy đủ)" + "Safe Mode (Full)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ an toàn (Đầy đủ)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安全模式(完整)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安全模式(完整)" } } } }, - "Safe Mode: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ an toàn: %@" + "Safe Mode: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ an toàn: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "安全模式:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安全模式:%@" } } } }, - "Same options will be applied to all selected tables.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cùng tùy chọn sẽ được áp dụng cho tất cả bảng đã chọn." + "Same options will be applied to all selected tables." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cùng tùy chọn sẽ được áp dụng cho tất cả bảng đã chọn." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "相同选项将应用于所有选中的表。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "相同选项将应用于所有选中的表。" } } } }, - "Sanitize formula-like values": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm sạch giá trị giống công thức" + "Sanitize formula-like values" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm sạch giá trị giống công thức" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清理类公式值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清理类公式值" } } } }, - "Save": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu" + "Save" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存" } } } }, - "Save and load filter presets": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu và tải mẫu bộ lọc" + "Save and load filter presets" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu và tải mẫu bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存和加载筛选预设" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存和加载筛选预设" } } } }, - "Save Anyway": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Vẫn lưu" + "Save Anyway" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vẫn lưu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "仍然保存" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仍然保存" } } } }, - "Save as Favorite": {}, - "Save as Favorite...": {}, - "Save as Preset...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu dưới dạng mẫu..." + "Save as Favorite" : { + + }, + "Save as Favorite..." : { + + }, + "Save as Preset..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu dưới dạng mẫu..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "另存为预设..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "另存为预设..." } } } }, - "Save as Template": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu dưới dạng mẫu" + "Save as Template" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu dưới dạng mẫu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "另存为模板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "另存为模板" } } } }, - "Save Changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu thay đổi" + "Save Changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu thay đổi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存更改" } } } }, - "Save Changes (⌘S)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu thay đổi (⌘S)" + "Save Changes (⌘S)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu thay đổi (⌘S)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存更改 (⌘S)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存更改 (⌘S)" } } } }, - "Save Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu thất bại" + "Save Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存失败" } } } }, - "Save Filter Preset": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu mẫu bộ lọc" + "Save Filter Preset" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu mẫu bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存筛选预设" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存筛选预设" } } } }, - "Save frequently used queries\nfor quick access.": {}, - "Save Sidebar Changes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu thay đổi thanh bên" + "Save frequently used queries\nfor quick access." : { + + }, + "Save Sidebar Changes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu thay đổi thanh bên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存侧边栏更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存侧边栏更改" } } } }, - "Save Table Template": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lưu mẫu bảng" + "Save Table Template" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lưu mẫu bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "保存表模板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存表模板" } } } }, - "Saved Connections": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối đã lưu" + "Saved Connections" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối đã lưu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已保存的连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已保存的连接" } } } }, - "SAVED CONNECTIONS": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "KẾT NỐI ĐÃ LƯU" + "SAVED CONNECTIONS" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "KẾT NỐI ĐÃ LƯU" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已保存的连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已保存的连接" } } } }, - "Scale": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tỉ lệ" + "Scale" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tỉ lệ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "小数位数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "小数位数" } } } }, - "Scale:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tỉ lệ:" + "Scale:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tỉ lệ:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "小数位数:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "小数位数:" } } } }, - "Schema": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Schema" + "Schema" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema" } } } }, - "Schema Switch Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển schema thất bại" + "Schema Switch Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển schema thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换 Schema 失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换 Schema 失败" } } } }, - "Schemas": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Schema" + "Schemas" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Schema" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schema" } } } }, - "Search columns...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm cột..." + "Search columns..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm cột..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索列..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索列..." } } } }, - "Search databases...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm cơ sở dữ liệu..." + "Search databases..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm cơ sở dữ liệu..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索数据库..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索数据库..." } } } }, - "Search for connection...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm kết nối..." + "Search for connection..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm kết nối..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索连接..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索连接..." } } } }, - "Search for field...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm trường..." + "Search for field..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm trường..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索字段..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索字段..." } } } }, - "Search or type...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm kiếm hoặc nhập..." + "Search or type..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm kiếm hoặc nhập..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索或输入..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索或输入..." } } } }, - "Search plugins...": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm kiếm plugin..." + "Search plugins..." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm kiếm plugin..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索插件..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索插件..." } } } }, - "Search queries...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm truy vấn..." + "Search queries..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm truy vấn..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索查询..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索查询..." } } } }, - "Search schemas...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm schema..." + "Search schemas..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm schema..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索 Schema..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索 Schema..." } } } }, - "Search shortcuts...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm phím tắt..." + "Search shortcuts..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm phím tắt..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索快捷键..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索快捷键..." } } } }, - "Search tables, views, databases...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm bảng, view, cơ sở dữ liệu..." + "Search tables, views, databases..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm bảng, view, cơ sở dữ liệu..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索表、视图、数据库..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索表、视图、数据库..." } } } }, - "Search...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tìm kiếm..." + "Search..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tìm kiếm..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "搜索..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索..." } } } }, - "Second value is required for BETWEEN": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị thứ hai là bắt buộc cho BETWEEN" + "Second value is required for BETWEEN" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị thứ hai là bắt buộc cho BETWEEN" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "BETWEEN 需要第二个值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "BETWEEN 需要第二个值" } } } }, - "Secondary": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Secondary" + "Secondary" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secondary" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Secondary" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secondary" } } } }, - "Secondary Preferred": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Secondary Preferred" + "Secondary Preferred" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secondary Preferred" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Secondary Preferred" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secondary Preferred" } } } }, - "Secondary Text": {}, - "Section Header": {}, - "SELECT * FROM users WHERE id = 1;": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SELECT * FROM users WHERE id = 1;" + "Secondary Text" : { + + }, + "Section Header" : { + + }, + "SELECT * FROM users WHERE id = 1;" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SELECT * FROM users WHERE id = 1;" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SELECT * FROM users WHERE id = 1;" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SELECT * FROM users WHERE id = 1;" } } } }, - "SELECT * FROM users WHERE id = 42;": {}, - "Select a Plugin": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn một Plugin" + "SELECT * FROM users WHERE id = 42;" : { + + }, + "Select a Plugin" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn một Plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择一个插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择一个插件" } } } }, - "Select a plugin to view details": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn plugin để xem chi tiết" + "Select a plugin to view details" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn plugin để xem chi tiết" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择插件查看详情" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择插件查看详情" } } } }, - "Select a Query": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn một truy vấn" + "Select a Query" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn một truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择一个查询" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择一个查询" } } } }, - "Select a row or table to view details": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn một hàng hoặc bảng để xem chi tiết" + "Select a row or table to view details" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn một hàng hoặc bảng để xem chi tiết" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择一行或一个表以查看详情" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择一行或一个表以查看详情" } } } }, - "Select a table to copy its structure:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn bảng để sao chép cấu trúc:" + "Select a table to copy its structure:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn bảng để sao chép cấu trúc:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择要复制结构的表:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择要复制结构的表:" } } } }, - "Select All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn tất cả" + "Select All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "全选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全选" } } } }, - "Select filter for %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn bộ lọc cho %@" + "Select filter for %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn bộ lọc cho %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "为 %@ 选择筛选条件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "为 %@ 选择筛选条件" } } } }, - "Select Plugin": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn Plugin" + "Select Plugin" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn Plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择插件" } } } }, - "Select SQL File...": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn tệp SQL..." + "Select SQL File..." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn tệp SQL..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择 SQL 文件..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择 SQL 文件..." } } } }, - "Select Tab %lld": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chọn tab %lld" + "Select Tab %lld" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chọn tab %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "选择标签页 %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择标签页 %lld" } } } }, - "Selected Item": {}, - "Selection": {}, - "Send Message": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gửi tin nhắn" + "Selected Item" : { + + }, + "Selection" : { + + }, + "Send Message" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gửi tin nhắn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "发送消息" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发送消息" } } } }, - "Server": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Máy chủ" + "Server" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máy chủ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "服务器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "服务器" } } } }, - "Server error (%lld): %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Server error (%1$lld): %2$@" + "Server error (%lld): %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Server error (%1$lld): %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi máy chủ (%1$lld): %2$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi máy chủ (%1$lld): %2$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "服务器错误 (%lld):%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "服务器错误 (%lld):%@" } } } }, - "Service Name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên dịch vụ" + "Service Name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên dịch vụ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "服务名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "服务名称" } } } }, - "Set DEFAULT": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt DEFAULT" + "Set DEFAULT" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt DEFAULT" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "设为 DEFAULT" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设为 DEFAULT" } } } }, - "Set EMPTY": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt TRỐNG" + "Set EMPTY" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt TRỐNG" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "设为空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设为空" } } } }, - "Set NULL": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt NULL" + "Set NULL" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt NULL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "设为 NULL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设为 NULL" } } } }, - "Set special value": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt giá trị đặc biệt" + "Set special value" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt giá trị đặc biệt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "设置特殊值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置特殊值" } } } }, - "Set Up AI Provider": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thiết lập nhà cung cấp AI" + "Set Up AI Provider" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thiết lập nhà cung cấp AI" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "设置 AI 提供商" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置 AI 提供商" } } } }, - "Set Value": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đặt giá trị" + "Set Value" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đặt giá trị" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "设置值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置值" } } } }, - "Settings were changed": {}, - "Settings were changed on both this Mac and another device.": {}, - "Settings:": {}, - "Shadows the SQL keyword '%@'": {}, - "Share anonymous usage data": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chia sẻ dữ liệu sử dụng ẩn danh" + "Settings were changed" : { + + }, + "Settings were changed on both this Mac and another device." : { + + }, + "Settings:" : { + + }, + "Shadows the SQL keyword '%@'" : { + + }, + "Share anonymous usage data" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chia sẻ dữ liệu sử dụng ẩn danh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "共享匿名使用数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "共享匿名使用数据" } } } }, - "Shell script to run before connecting. Non-zero exit aborts connection.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Shell script chạy trước khi kết nối. Exit code khác 0 sẽ hủy kết nối." + "Shell script to run before connecting. Non-zero exit aborts connection." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shell script chạy trước khi kết nối. Exit code khác 0 sẽ hủy kết nối." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接前运行的 Shell 脚本。非零退出码将中止连接。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接前运行的 Shell 脚本。非零退出码将中止连接。" } } } }, - "Shortcut Conflict": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xung đột phím tắt" + "Shortcut Conflict" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xung đột phím tắt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快捷键冲突" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷键冲突" } } } }, - "Shortcut recorder": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Ghi phím tắt" + "Shortcut recorder" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ghi phím tắt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "快捷键录制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷键录制" } } } }, - "Show All": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị tất cả" + "Show All" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị tất cả" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示全部" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示全部" } } } }, - "Show All %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị tất cả %@" + "Show All %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị tất cả %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示全部%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示全部%@" } } } }, - "Show All Collections": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị tất cả Collection" + "Show All Collections" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị tất cả Collection" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示所有集合" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示所有集合" } } } }, - "Show All Databases": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiển thị tất cả cơ sở dữ liệu" + "Show All Databases" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiển thị tất cả cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示所有数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示所有数据库" } } } }, - "Show All Tables": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện tất cả bảng" + "Show All Tables" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện tất cả bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示所有表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示所有表" } } } }, - "Show alternate row backgrounds": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện nền xen kẽ dòng" + "Show alternate row backgrounds" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện nền xen kẽ dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示交替行背景" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示交替行背景" } } } }, - "Show line numbers": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện số dòng" + "Show line numbers" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện số dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示行号" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示行号" } } } }, - "Show Next Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện tab tiếp" + "Show Next Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện tab tiếp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示下一个标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示下一个标签页" } } } }, - "Show Previous Tab": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện tab trước" + "Show Previous Tab" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện tab trước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示上一个标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示上一个标签页" } } } }, - "Show Structure": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện cấu trúc" + "Show Structure" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện cấu trúc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示结构" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示结构" } } } }, - "Show Welcome Screen": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện màn hình chào mừng" + "Show Welcome Screen" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện màn hình chào mừng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示欢迎屏幕" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示欢迎屏幕" } } } }, - "Show Welcome Window": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hiện cửa sổ chào mừng" + "Show Welcome Window" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện cửa sổ chào mừng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示欢迎窗口" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示欢迎窗口" } } } }, - "Sidebar": {}, - "Sidebar Panel": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thanh bên" + "Sidebar" : { + + }, + "Sidebar Panel" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thanh bên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "侧边栏" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "侧边栏" } } } }, - "Sign in to iCloud in System Settings to enable sync.": {}, - "Sign in to iCloud to enable sync": {}, - "Silent": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Im lặng" + "Sign in to iCloud in System Settings to enable sync." : { + + }, + "Sign in to iCloud to enable sync" : { + + }, + "Silent" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im lặng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "静默" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "静默" } } } }, - "Single-clicking a table opens a temporary tab that gets replaced on next click.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhấp một lần vào bảng sẽ mở tab tạm thời, tab này sẽ được thay thế khi nhấp tiếp." + "Single-clicking a table opens a temporary tab that gets replaced on next click." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhấp một lần vào bảng sẽ mở tab tạm thời, tab này sẽ được thay thế khi nhấp tiếp." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "单击表时打开临时标签页,下次点击时会被替换。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "单击表时打开临时标签页,下次点击时会被替换。" } } } }, - "Size": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích thước" + "Size" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích thước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "大小" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大小" } } } }, - "SIZE": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "KÍCH THƯỚC" + "SIZE" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "KÍCH THƯỚC" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "大小" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大小" } } } }, - "Size:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kích thước:" + "Size:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kích thước:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "大小:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大小:" } } } }, - "Skip": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bỏ qua" + "Skip" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bỏ qua" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "跳过" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跳过" } } } }, - "Slow": {}, - "Small": {}, - "Smooth": {}, - "Software Update": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cập nhật phần mềm" + "Slow" : { + + }, + "Small" : { + + }, + "Smooth" : { + + }, + "Software Update" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cập nhật phần mềm" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "软件更新" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "软件更新" } } } }, - "Sorting will reload data and discard all unsaved changes.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sắp xếp sẽ tải lại dữ liệu và hủy tất cả thay đổi chưa lưu." + "Sorting will reload data and discard all unsaved changes." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sắp xếp sẽ tải lại dữ liệu và hủy tất cả thay đổi chưa lưu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "排序将重新加载数据并丢弃所有未保存的更改。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "排序将重新加载数据并丢弃所有未保存的更改。" } } } }, - "Source:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nguồn:" + "Source:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nguồn:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "来源:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "来源:" } } } }, - "Spacing": {}, - "Spacious": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Rộng rãi" + "Spacing" : { + + }, + "Spacious" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rộng rãi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "宽松" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "宽松" } } } }, - "Sponsor": {}, - "Sponsor TablePro": {}, - "SQL": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SQL" + "Sponsor" : { + + }, + "Sponsor TablePro" : { + + }, + "SQL" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SQL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL" } } } }, - "SQL commands to run after connecting, e.g. SET time_zone = 'Asia/Ho_Chi_Minh'. One per line or separated by semicolons.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Các lệnh SQL chạy sau khi kết nối, ví dụ SET time_zone = 'Asia/Ho_Chi_Minh'. Mỗi dòng một lệnh hoặc phân cách bằng dấu chấm phẩy." + "SQL commands to run after connecting, e.g. SET time_zone = 'Asia/Ho_Chi_Minh'. One per line or separated by semicolons." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Các lệnh SQL chạy sau khi kết nối, ví dụ SET time_zone = 'Asia/Ho_Chi_Minh'. Mỗi dòng một lệnh hoặc phân cách bằng dấu chấm phẩy." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接后执行的 SQL 命令,例如 SET time_zone = 'Asia/Ho_Chi_Minh'。每行一条或用分号分隔。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接后执行的 SQL 命令,例如 SET time_zone = 'Asia/Ho_Chi_Minh'。每行一条或用分号分隔。" } } } }, - "SQL Dialect": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phương ngữ SQL" + "SQL Dialect" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phương ngữ SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SQL 方言" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL 方言" } } } }, - "SQL Functions": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hàm SQL" + "SQL Functions" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hàm SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SQL 函数" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL 函数" } } } }, - "SQL import is not supported for %@ connections.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập SQL không được hỗ trợ cho kết nối %@." + "SQL import is not supported for %@ connections." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập SQL không được hỗ trợ cho kết nối %@." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持对 %@ 连接进行 SQL 导入。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持对 %@ 连接进行 SQL 导入。" } } } }, - "SQL Preview": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem trước SQL" + "SQL Preview" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem trước SQL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SQL 预览" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL 预览" } } } }, - "SQL Server": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SQL Server" + "SQL Server" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL Server" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SQL Server" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQL Server" } } } }, - "SQLite is file-based": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SQLite dựa trên tệp" + "SQLite is file-based" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQLite dựa trên tệp" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SQLite 基于文件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SQLite 基于文件" } } } }, - "SSH Agent": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SSH Agent" + "SSH Agent" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH Agent" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH Agent" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH Agent" } } } }, - "SSH authentication failed. Check your credentials or private key.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực SSH thất bại. Kiểm tra thông tin đăng nhập hoặc khóa riêng tư." + "SSH authentication failed. Check your credentials or private key." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực SSH thất bại. Kiểm tra thông tin đăng nhập hoặc khóa riêng tư." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 认证失败。请检查您的凭据或私钥。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 认证失败。请检查您的凭据或私钥。" } } } }, - "SSH command not found. Please ensure OpenSSH is installed.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không tìm thấy lệnh SSH. Vui lòng đảm bảo OpenSSH đã được cài đặt." + "SSH command not found. Please ensure OpenSSH is installed." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không tìm thấy lệnh SSH. Vui lòng đảm bảo OpenSSH đã được cài đặt." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未找到 SSH 命令。请确保已安装 OpenSSH。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到 SSH 命令。请确保已安装 OpenSSH。" } } } }, - "SSH connection timed out": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kết nối SSH đã hết thời gian" + "SSH connection timed out" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kết nối SSH đã hết thời gian" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 连接超时" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 连接超时" } } } }, - "SSH Host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Máy chủ SSH" + "SSH Host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máy chủ SSH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 主机" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 主机" } } } }, - "SSH Host Key Changed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khóa máy chủ SSH đã thay đổi" + "SSH Host Key Changed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khóa máy chủ SSH đã thay đổi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 主机密钥已更改" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 主机密钥已更改" } } } }, - "SSH host key verification failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác minh khóa máy chủ SSH thất bại" + "SSH host key verification failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác minh khóa máy chủ SSH thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 主机密钥验证失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 主机密钥验证失败" } } } }, - "SSH Port": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cổng SSH" + "SSH Port" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cổng SSH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 端口" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 端口" } } } }, - "SSH Tunnel": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đường hầm SSH" + "SSH Tunnel" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đường hầm SSH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 隧道" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 隧道" } } } }, - "SSH tunnel already exists for connection: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đường hầm SSH đã tồn tại cho kết nối: %@" + "SSH tunnel already exists for connection: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đường hầm SSH đã tồn tại cho kết nối: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "连接已存在 SSH 隧道:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接已存在 SSH 隧道:%@" } } } }, - "SSH tunnel creation failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạo đường hầm SSH thất bại: %@" + "SSH tunnel creation failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạo đường hầm SSH thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "创建 SSH 隧道失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建 SSH 隧道失败:%@" } } } }, - "SSH User": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Người dùng SSH" + "SSH User" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Người dùng SSH" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 用户" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSH 用户" } } } }, - "ssh.example.com": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "ssh.example.com" + "ssh.example.com" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ssh.example.com" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "ssh.example.com" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ssh.example.com" } } } }, - "SSL Mode": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ SSL" + "SSL Mode" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ SSL" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSL 模式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSL 模式" } } } }, - "SSL/TLS": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "SSL/TLS" + "SSL/TLS" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSL/TLS" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSL/TLS" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSL/TLS" } } } }, - "starts with": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "bắt đầu bằng" + "starts with" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "bắt đầu bằng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "以...开头" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "以...开头" } } } }, - "Startup Commands": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lệnh khởi động" + "Startup Commands" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lệnh khởi động" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启动命令" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启动命令" } } } }, - "statement": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "câu lệnh" + "statement" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "câu lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "条语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "条语句" } } } }, - "Statement %lld": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Câu lệnh %lld" + "Statement %lld" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Câu lệnh %lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "语句 %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语句 %lld" } } } }, - "Statement %lld of %lld": { - "extractionState": "stale", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Statement %1$lld of %2$lld" + "Statement %lld of %lld" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Statement %1$lld of %2$lld" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Câu lệnh %1$lld / %2$lld" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Câu lệnh %1$lld / %2$lld" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "语句 %lld / %lld" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语句 %lld / %lld" } } } }, - "Statement:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Câu lệnh:" + "Statement:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Câu lệnh:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "语句:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语句:" } } } }, - "statements": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "câu lệnh" + "statements" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "câu lệnh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "条语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "条语句" } } } }, - "STATISTICS": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "THỐNG KÊ" + "STATISTICS" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "THỐNG KÊ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "统计信息" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "统计信息" } } } }, - "Status": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trạng thái" + "Status" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trạng thái" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "状态" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "状态" } } } }, - "Status Colors": {}, - "Status Dot": {}, - "Stop": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dừng" + "Status Colors" : { + + }, + "Status Dot" : { + + }, + "Stop" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dừng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "停止" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停止" } } } }, - "Stop Generating": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Dừng tạo phản hồi" + "Stop Generating" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dừng tạo phản hồi" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "停止生成" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "停止生成" } } } }, - "Streaming failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phát trực tuyến thất bại: %@" + "Streaming failed: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phát trực tuyến thất bại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "流式传输失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "流式传输失败:%@" } } } }, - "String": {}, - "Structure": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cấu trúc" + "String" : { + + }, + "Structure" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cấu trúc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "结构" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "结构" } } } }, - "Structure, Drop, and Data options are configured per table in the table list.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tùy chọn Cấu trúc, Xóa và Dữ liệu được cấu hình cho từng bảng trong danh sách bảng." + "Structure, Drop, and Data options are configured per table in the table list." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tùy chọn Cấu trúc, Xóa và Dữ liệu được cấu hình cho từng bảng trong danh sách bảng." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "结构、删除和数据选项在表列表中按表单独配置。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "结构、删除和数据选项在表列表中按表单独配置。" } } } }, - "Success": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thành công" + "Success" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thành công" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "成功" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "成功" } } } }, - "Suspended": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạm ngưng" + "Suspended" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạm ngưng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已暂停" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已暂停" } } } }, - "Switch connection": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển kết nối" + "Switch connection" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换连接" } } } }, - "Switch Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển kết nối" + "Switch Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换连接" } } } }, - "Switch Connection (⌘⌥C)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển kết nối (⌘⌥C)" + "Switch Connection (⌘⌥C)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển kết nối (⌘⌥C)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换连接 (⌘⌥C)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换连接 (⌘⌥C)" } } } }, - "Switch Connection...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển kết nối..." + "Switch Connection..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển kết nối..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换连接..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换连接..." } } } }, - "Switch Database": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển cơ sở dữ liệu" + "Switch Database" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển cơ sở dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换数据库" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换数据库" } } } }, - "Switch Database (⌘K)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển cơ sở dữ liệu (⌘K)" + "Switch Database (⌘K)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chuyển cơ sở dữ liệu (⌘K)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换数据库 (⌘K)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换数据库 (⌘K)" } } } }, - "Sync": {}, - "Sync Categories": {}, - "Sync Conflict": {}, - "Sync connections, settings, and history across your Macs.": {}, - "Sync Error": {}, - "Sync Now": {}, - "Sync Off": {}, - "Sync paused — Pro license expired": {}, - "Sync zone not found. A full sync will be performed.": {}, - "Synced": {}, - "Syncing with iCloud...": {}, - "Syncing...": {}, - "Syncs connections, settings, and history across your Macs via iCloud.": {}, - "Syntax Colors": {}, - "System": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hệ thống" + "Sync" : { + + }, + "Sync Categories" : { + + }, + "Sync Conflict" : { + + }, + "Sync connections, settings, and history across your Macs." : { + + }, + "Sync Error" : { + + }, + "Sync Now" : { + + }, + "Sync Off" : { + + }, + "Sync paused — Pro license expired" : { + + }, + "Sync zone not found. A full sync will be performed." : { + + }, + "Synced" : { + + }, + "Syncing with iCloud..." : { + + }, + "Syncing..." : { + + }, + "Syncs connections, settings, and history across your Macs via iCloud." : { + + }, + "Syntax Colors" : { + + }, + "System" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hệ thống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "系统" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "系统" } } } }, - "System Reserved Shortcut": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phím tắt hệ thống" + "System Reserved Shortcut" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phím tắt hệ thống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "系统保留快捷键" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "系统保留快捷键" } } } }, - "System Table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng hệ thống" + "System Table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng hệ thống" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "系统表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "系统表" } } } }, - "Tab Behavior": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hành vi tab" + "Tab Behavior" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hành vi tab" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "标签页行为" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签页行为" } } } }, - "Tab width:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Độ rộng tab:" + "Tab width:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Độ rộng tab:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "制表符宽度:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "制表符宽度:" } } } }, - "Table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng" + "Table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表" } } } }, - "Table '%@' has no columns or does not exist": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng '%@' không có cột hoặc không tồn tại" + "Table '%@' has no columns or does not exist" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng '%@' không có cột hoặc không tồn tại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表 '%@' 没有列或不存在" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表 '%@' 没有列或不存在" } } } }, - "Table creation options not available": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tùy chọn tạo bảng không khả dụng" + "Table creation options not available" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tùy chọn tạo bảng không khả dụng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表创建选项不可用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表创建选项不可用" } } } }, - "Table Info": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thông tin bảng" + "Table Info" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thông tin bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表信息" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表信息" } } } }, - "Table Name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên bảng" + "Table Name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表名" } } } }, - "Table: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng: %@" + "Table: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表:%@" } } } }, - "TablePro": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TablePro" + "TablePro" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "TablePro" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro" } } } }, - "TablePro Website": {}, - "Tables": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bảng" + "TablePro Website" : { + + }, + "Tables" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表" } } } }, - "Tablespace": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tablespace" + "Tablespace" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tablespace" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "表空间" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "表空间" } } } }, - "Tabs": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tab" + "Tabs" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tab" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签页" } } } }, - "Tag": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhãn" + "Tag" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhãn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "标签" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签" } } } }, - "Tag name": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên thẻ" + "Tag name" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên thẻ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "标签名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签名称" } } } }, - "Tag: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thẻ: %@" + "Tag: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thẻ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "标签:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签:%@" } } } }, - "Template": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mẫu" + "Template" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mẫu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "模板" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "模板" } } } }, - "Template Name": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên mẫu" + "Template Name" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên mẫu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "模板名称" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "模板名称" } } } }, - "Temporarily disable foreign key constraints during import. Useful for importing data with circular dependencies.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tạm thời tắt ràng buộc khóa ngoại trong quá trình nhập. Hữu ích khi nhập dữ liệu có phụ thuộc vòng." + "Temporarily disable foreign key constraints during import. Useful for importing data with circular dependencies." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tạm thời tắt ràng buộc khóa ngoại trong quá trình nhập. Hữu ích khi nhập dữ liệu có phụ thuộc vòng." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "导入时临时禁用外键约束。适用于导入具有循环依赖的数据。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入时临时禁用外键约束。适用于导入具有循环依赖的数据。" } } } }, - "Tertiary Text": {}, - "Test": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểm tra" + "Tertiary Text" : { + + }, + "Test" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "测试" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "测试" } } } }, - "Test Connection": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểm tra kết nối" + "Test Connection" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra kết nối" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "测试连接" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "测试连接" } } } }, - "Text": {}, - "The %@ plugin is not installed. Would you like to download it from the plugin marketplace?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin %@ chưa được cài đặt. Bạn có muốn tải về từ chợ plugin?" + "Text" : { + + }, + "The %@ plugin is not installed. Would you like to download it from the plugin marketplace?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin %@ chưa được cài đặt. Bạn có muốn tải về từ chợ plugin?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 插件未安装。是否要从插件市场下载?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 插件未安装。是否要从插件市场下载?" } } } }, - "The %@ plugin is not installed. You can download it from the plugin marketplace.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin %@ chưa được cài đặt. Bạn có thể tải về từ chợ plugin." + "The %@ plugin is not installed. You can download it from the plugin marketplace." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin %@ chưa được cài đặt. Bạn có thể tải về từ chợ plugin." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "%@ 插件未安装。您可以从插件市场下载。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 插件未安装。您可以从插件市场下载。" } } } }, - "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": { - "stringUnit": { - "state": "new", - "value": "The authenticity of host '%1$@' can't be established.\n\n%2$@ key fingerprint is:\n%3$@\n\nAre you sure you want to continue connecting?" + "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" : { + "stringUnit" : { + "state" : "new", + "value" : "The authenticity of host '%1$@' can't be established.\n\n%2$@ key fingerprint is:\n%3$@\n\nAre you sure you want to continue connecting?" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không thể xác minh tính xác thực của máy chủ '%@'.\n\nVân tay khóa %@ là:\n%@\n\nBạn có chắc chắn muốn tiếp tục kết nối?" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể xác minh tính xác thực của máy chủ '%@'.\n\nVân tay khóa %@ là:\n%@\n\nBạn có chắc chắn muốn tiếp tục kết nối?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无法确认主机 '%@' 的真实性。\n\n%@ 密钥指纹为:\n%@\n\n确定要继续连接吗?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法确认主机 '%@' 的真实性。\n\n%@ 密钥指纹为:\n%@\n\n确定要继续连接吗?" } } } }, - "The folder \"%@\" will be deleted. Items inside will be moved to the parent level.": {}, - "The following %lld queries may permanently modify or delete data. This action cannot be undone.\n\n%@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "The following %1$lld queries may permanently modify or delete data. This action cannot be undone.\n\n%2$@" + "The folder \"%@\" will be deleted. Items inside will be moved to the parent level." : { + + }, + "The following %lld queries may permanently modify or delete data. This action cannot be undone.\n\n%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The following %1$lld queries may permanently modify or delete data. This action cannot be undone.\n\n%2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "%1$lld truy vấn sau có thể thay đổi hoặc xóa dữ liệu vĩnh viễn. Hành động này không thể hoàn tác.\n\n%2$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld truy vấn sau có thể thay đổi hoặc xóa dữ liệu vĩnh viễn. Hành động này không thể hoàn tác.\n\n%2$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "以下 %lld 个查询可能会永久修改或删除数据。此操作无法撤销。\n\n%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "以下 %lld 个查询可能会永久修改或删除数据。此操作无法撤销。\n\n%@" } } } }, - "The text is not valid JSON. Save anyway?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nội dung không phải JSON hợp lệ. Vẫn lưu?" + "The text is not valid JSON. Save anyway?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nội dung không phải JSON hợp lệ. Vẫn lưu?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "文本不是有效的 JSON。仍然保存?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文本不是有效的 JSON。仍然保存?" } } } }, - "Themes": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giao diện" + "Themes" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giao diện" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "主题" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主题" } } } }, - "This database has no %@ yet.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu này chưa có %@ nào." + "This database has no %@ yet." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu này chưa có %@ nào." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此数据库还没有%@。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此数据库还没有%@。" } } } }, - "This database has no tables yet.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cơ sở dữ liệu này chưa có bảng nào." + "This database has no tables yet." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cơ sở dữ liệu này chưa có bảng nào." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此数据库还没有表。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此数据库还没有表。" } } } }, - "This DELETE query has no WHERE clause and will delete ALL rows in the table. This action cannot be undone.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn DELETE này không có mệnh đề WHERE và sẽ xóa TẤT CẢ dòng trong bảng. Thao tác này không thể hoàn tác." + "This DELETE query has no WHERE clause and will delete ALL rows in the table. This action cannot be undone." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn DELETE này không có mệnh đề WHERE và sẽ xóa TẤT CẢ dòng trong bảng. Thao tác này không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此 DELETE 查询没有 WHERE 子句,将删除表中的所有行。此操作无法撤销。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此 DELETE 查询没有 WHERE 子句,将删除表中的所有行。此操作无法撤销。" } } } }, - "This DROP query will permanently remove database objects. This action cannot be undone.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn DROP này sẽ xóa vĩnh viễn các đối tượng cơ sở dữ liệu. Thao tác này không thể hoàn tác." + "This DROP query will permanently remove database objects. This action cannot be undone." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn DROP này sẽ xóa vĩnh viễn các đối tượng cơ sở dữ liệu. Thao tác này không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此 DROP 查询将永久删除数据库对象。此操作无法撤销。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此 DROP 查询将永久删除数据库对象。此操作无法撤销。" } } } }, - "This is a built-in theme.": {}, - "This is a registry theme.": {}, - "This keyword is already in use": {}, - "This Mac": {}, - "This Month": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tháng này" + "This is a built-in theme." : { + + }, + "This is a registry theme." : { + + }, + "This keyword is already in use" : { + + }, + "This Mac" : { + + }, + "This Month" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tháng này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "本月" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "本月" } } } }, - "This operation is not supported": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thao tác này không được hỗ trợ" + "This operation is not supported" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thao tác này không được hỗ trợ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持此操作" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持此操作" } } } }, - "This plugin requires TablePro %@ or later": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Plugin này yêu cầu TablePro %@ trở lên" + "This plugin requires TablePro %@ or later" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plugin này yêu cầu TablePro %@ trở lên" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此插件需要 TablePro %@ 或更高版本" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此插件需要 TablePro %@ 或更高版本" } } } }, - "This query may permanently modify or delete data.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn này có thể sửa đổi hoặc xóa dữ liệu vĩnh viễn." + "This query may permanently modify or delete data." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn này có thể sửa đổi hoặc xóa dữ liệu vĩnh viễn." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此查询可能会永久修改或删除数据。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此查询可能会永久修改或删除数据。" } } } }, - "This shortcut is reserved by macOS and cannot be assigned.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phím tắt này được macOS dành riêng và không thể gán." + "This shortcut is reserved by macOS and cannot be assigned." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phím tắt này được macOS dành riêng và không thể gán." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此快捷键已被 macOS 保留,无法分配。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此快捷键已被 macOS 保留,无法分配。" } } } }, - "This SQL query failed with an error. Please fix it.\n\nQuery:\n```sql\n%@\n```\n\nError: %@": { - "extractionState": "stale", - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "This SQL query failed with an error. Please fix it.\n\nQuery:\n```sql\n%1$@\n```\n\nError: %2$@" + "This SQL query failed with an error. Please fix it.\n\nQuery:\n```sql\n%@\n```\n\nError: %@" : { + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This SQL query failed with an error. Please fix it.\n\nQuery:\n```sql\n%1$@\n```\n\nError: %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Câu truy vấn SQL sau đã thất bại với lỗi. Vui lòng sửa lỗi.\n\nTruy vấn:\n```sql\n%1$@\n```\n\nLỗi: %2$@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Câu truy vấn SQL sau đã thất bại với lỗi. Vui lòng sửa lỗi.\n\nTruy vấn:\n```sql\n%1$@\n```\n\nLỗi: %2$@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此 SQL 查询执行失败并报错。请修复。\n\n查询:\n```sql\n%@\n```\n\n错误:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此 SQL 查询执行失败并报错。请修复。\n\n查询:\n```sql\n%@\n```\n\n错误:%@" } } } }, - "This TRUNCATE query will permanently delete all rows in the table. This action cannot be undone.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truy vấn TRUNCATE này sẽ xóa vĩnh viễn tất cả dòng trong bảng. Thao tác này không thể hoàn tác." + "This TRUNCATE query will permanently delete all rows in the table. This action cannot be undone." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truy vấn TRUNCATE này sẽ xóa vĩnh viễn tất cả dòng trong bảng. Thao tác này không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此 TRUNCATE 查询将永久删除表中的所有行。此操作无法撤销。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此 TRUNCATE 查询将永久删除表中的所有行。此操作无法撤销。" } } } }, - "This Week": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tuần này" + "This Week" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuần này" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "本周" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "本周" } } } }, - "This will permanently delete %lld %@. This action cannot be undone.": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "This will permanently delete %1$lld %2$@. This action cannot be undone." + "This will permanently delete %lld %@. This action cannot be undone." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "This will permanently delete %1$lld %2$@. This action cannot be undone." } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thao tác này sẽ xóa vĩnh viễn %1$lld %2$@. Không thể hoàn tác." + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thao tác này sẽ xóa vĩnh viễn %1$lld %2$@. Không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此操作将永久删除 %lld 个%@。无法撤销。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此操作将永久删除 %lld 个%@。无法撤销。" } } } }, - "This will permanently delete all query history entries. This action cannot be undone.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thao tác này sẽ xóa vĩnh viễn toàn bộ lịch sử truy vấn. Không thể hoàn tác." + "This will permanently delete all query history entries. This action cannot be undone." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thao tác này sẽ xóa vĩnh viễn toàn bộ lịch sử truy vấn. Không thể hoàn tác." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此操作将永久删除所有查询历史记录。无法撤销。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此操作将永久删除所有查询历史记录。无法撤销。" } } } }, - "This will remove the license from this machine. You can reactivate later.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thao tác này sẽ gỡ giấy phép khỏi máy này. Bạn có thể kích hoạt lại sau." + "This will remove the license from this machine. You can reactivate later." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thao tác này sẽ gỡ giấy phép khỏi máy này. Bạn có thể kích hoạt lại sau." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "此操作将从本机移除许可证。您可以稍后重新激活。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此操作将从本机移除许可证。您可以稍后重新激活。" } } } }, - "TIMESTAMPS": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "THỜI GIAN" + "TIMESTAMPS" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "THỜI GIAN" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "时间戳" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时间戳" } } } }, - "Tiny": {}, - "Tiny Dot": {}, - "Title 2": {}, - "Title 3": {}, - "to view data": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "để xem dữ liệu" + "Tiny" : { + + }, + "Tiny Dot" : { + + }, + "Title 2" : { + + }, + "Title 3" : { + + }, + "TLS Mode" : { + + }, + "to view data" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "để xem dữ liệu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "以查看数据" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "以查看数据" } } } }, - "Today": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hôm nay" + "Today" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hôm nay" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "今天" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今天" } } } }, - "Toggle AI Chat": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt AI Chat" + "Toggle AI Chat" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt AI Chat" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换 AI 聊天" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换 AI 聊天" } } } }, - "Toggle AI Chat (⌘⇧L)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt AI Chat (⌘⇧L)" + "Toggle AI Chat (⌘⇧L)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt AI Chat (⌘⇧L)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换 AI 聊天 (⌘⇧L)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换 AI 聊天 (⌘⇧L)" } } } }, - "Toggle filters": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt bộ lọc" + "Toggle filters" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换筛选" } } } }, - "Toggle Filters": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt bộ lọc" + "Toggle Filters" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt bộ lọc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换筛选" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换筛选" } } } }, - "Toggle Filters (⌘F)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt bộ lọc (⌘F)" + "Toggle Filters (⌘F)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt bộ lọc (⌘F)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换筛选 (⌘F)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换筛选 (⌘F)" } } } }, - "Toggle Filters (Cmd+F)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt bộ lọc (Cmd+F)" + "Toggle Filters (Cmd+F)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt bộ lọc (Cmd+F)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换筛选 (Cmd+F)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换筛选 (Cmd+F)" } } } }, - "Toggle History": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt lịch sử" + "Toggle History" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt lịch sử" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换历史记录" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换历史记录" } } } }, - "Toggle inspector": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt thanh kiểm tra" + "Toggle inspector" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt thanh kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换检查器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换检查器" } } } }, - "Toggle Inspector": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt thanh kiểm tra" + "Toggle Inspector" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt thanh kiểm tra" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换检查器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换检查器" } } } }, - "Toggle Inspector (⌘⌥B)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt thanh kiểm tra (⌘⌥B)" + "Toggle Inspector (⌘⌥B)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt thanh kiểm tra (⌘⌥B)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换检查器 (⌘⌥B)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换检查器 (⌘⌥B)" } } } }, - "Toggle query history": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt lịch sử truy vấn" + "Toggle query history" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt lịch sử truy vấn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换查询历史" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换查询历史" } } } }, - "Toggle Query History (⌘⇧H)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt lịch sử truy vấn (⌘⇧H)" + "Toggle Query History (⌘⇧H)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt lịch sử truy vấn (⌘⇧H)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换查询历史 (⌘⇧H)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换查询历史 (⌘⇧H)" } } } }, - "Toggle Query History (⌘Y)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt lịch sử truy vấn (⌘Y)" + "Toggle Query History (⌘Y)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt lịch sử truy vấn (⌘Y)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换查询历史 (⌘Y)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换查询历史 (⌘Y)" } } } }, - "Toggle Table Browser": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bật/tắt trình duyệt bảng" + "Toggle Table Browser" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật/tắt trình duyệt bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "切换表浏览器" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换表浏览器" } } } }, - "Toolbar": {}, - "Total Size": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tổng kích thước" + "Toolbar" : { + + }, + "Total Size" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng kích thước" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "总大小" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "总大小" } } } }, - "TOTP": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TOTP" + "TOTP" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "TOTP" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP" } } } }, - "TOTP Secret": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mã bí mật TOTP" + "TOTP Secret" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mã bí mật TOTP" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "TOTP 密钥" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP 密钥" } } } }, - "true": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "true" + "true" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "true" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "true" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "true" } } } }, - "TRUE": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "TRUE" + "TRUE" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "TRUE" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "TRUE" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TRUE" } } } }, - "Truncate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truncate" + "Truncate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truncate" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清空" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空" } } } }, - "Truncate %lld tables": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm trống %lld bảng" + "Truncate %lld tables" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm trống %lld bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清空 %lld 个表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空 %lld 个表" } } } }, - "Truncate all tables linked by foreign keys": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xóa dữ liệu tất cả bảng liên kết bằng khóa ngoại" + "Truncate all tables linked by foreign keys" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xóa dữ liệu tất cả bảng liên kết bằng khóa ngoại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清空所有通过外键关联的表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空所有通过外键关联的表" } } } }, - "Truncate Table": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Truncate bảng" + "Truncate Table" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Truncate bảng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清空表" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空表" } } } }, - "Truncate table '%@'": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Làm trống bảng '%@'" + "Truncate table '%@'" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Làm trống bảng '%@'" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "清空表 '%@'" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空表 '%@'" } } } }, - "Trust": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tin cậy" + "Trust" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tin cậy" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "信任" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "信任" } } } }, - "Try a different search term": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thử từ khoá tìm kiếm khác" + "Try a different search term" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử từ khoá tìm kiếm khác" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尝试其他搜索词" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尝试其他搜索词" } } } }, - "Try adjusting your search terms\nor date filter.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thử điều chỉnh từ khoá tìm kiếm\nhoặc bộ lọc ngày." + "Try adjusting your search terms\nor date filter." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử điều chỉnh từ khoá tìm kiếm\nhoặc bộ lọc ngày." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "尝试调整搜索词\n或日期筛选。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "尝试调整搜索词\n或日期筛选。" } } } }, - "Try Again": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thử lại" + "Try Again" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thử lại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重试" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" } } } }, - "Two-Factor Authentication": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực hai yếu tố" + "Two-Factor Authentication" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực hai yếu tố" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "双因素认证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "双因素认证" } } } }, - "Type": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kiểu" + "Type" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "类型" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "类型" } } } }, - "Type shortcut...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nhập phím tắt..." + "Type shortcut..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nhập phím tắt..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "输入快捷键..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入快捷键..." } } } }, - "Typography": {}, - "Undo": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hoàn tác" + "Typography" : { + + }, + "Undo" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoàn tác" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "撤销" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "撤销" } } } }, - "Undo Delete": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hoàn tác xóa" + "Undo Delete" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoàn tác xóa" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "撤销删除" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "撤销删除" } } } }, - "Uninstall": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gỡ cài đặt" + "Uninstall" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gỡ cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "卸载" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "卸载" } } } }, - "Uninstall %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gỡ cài đặt %@" + "Uninstall %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gỡ cài đặt %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "卸载 %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "卸载 %@" } } } }, - "Uninstall Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gỡ cài đặt thất bại" + "Uninstall Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gỡ cài đặt thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "卸载失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "卸载失败" } } } }, - "Uninstall plugin": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gỡ cài đặt plugin" + "Uninstall plugin" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gỡ cài đặt plugin" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "卸载插件" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "卸载插件" } } } }, - "Uninstall Plugin?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Gỡ cài đặt Plugin?" + "Uninstall Plugin?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gỡ cài đặt Plugin?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "卸载插件?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "卸载插件?" } } } }, - "Unique": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Duy nhất" + "Unique" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duy nhất" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "唯一" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "唯一" } } } }, - "UNIQUE": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "UNIQUE" + "UNIQUE" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "UNIQUE" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "UNIQUE" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "UNIQUE" } } } }, - "Unknown error": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lỗi không xác định" + "Unknown error" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi không xác định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未知错误" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知错误" } } } }, - "Unknown SSH Host": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Máy chủ SSH không xác định" + "Unknown SSH Host" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Máy chủ SSH không xác định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未知 SSH 主机" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知 SSH 主机" } } } }, - "Unlicensed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chưa có giấy phép" + "Unlicensed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chưa có giấy phép" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未授权" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未授权" } } } }, - "Unlimited": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không giới hạn" + "Unlimited" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không giới hạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无限制" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无限制" } } } }, - "Unset": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bỏ đặt" + "Unset" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bỏ đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "取消设置" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消设置" } } } }, - "Unsigned": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không dấu" + "Unsigned" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không dấu" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "无符号" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无符号" } } } }, - "Unsupported database scheme: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Scheme cơ sở dữ liệu không được hỗ trợ: %@" + "Unsupported database scheme: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scheme cơ sở dữ liệu không được hỗ trợ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持的数据库方案:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持的数据库方案:%@" } } } }, - "Unsupported MongoDB method: %@": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phương thức MongoDB không được hỗ trợ: %@" + "Unsupported MongoDB method: %@" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phương thức MongoDB không được hỗ trợ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持的 MongoDB 方法:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持的 MongoDB 方法:%@" } } } }, - "Unsupported schema operation: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thao tác schema không được hỗ trợ: %@" + "Unsupported schema operation: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thao tác schema không được hỗ trợ: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不支持的 Schema 操作:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不支持的 Schema 操作:%@" } } } }, - "Untitled": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có tiêu đề" + "Untitled" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có tiêu đề" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "未命名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未命名" } } } }, - "UPDATE Statement(s)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Câu lệnh UPDATE" + "UPDATE Statement(s)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Câu lệnh UPDATE" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "UPDATE 语句" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "UPDATE 语句" } } } }, - "Updated": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã cập nhật" + "Updated" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã cập nhật" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已更新" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已更新" } } } }, - "US Long (12/31/2024 11:59:59 PM)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mỹ dài (12/31/2024 11:59:59 PM)" + "US Long (12/31/2024 11:59:59 PM)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mỹ dài (12/31/2024 11:59:59 PM)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "美式长格式 (12/31/2024 11:59:59 PM)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "美式长格式 (12/31/2024 11:59:59 PM)" } } } }, - "US Short (12/31/2024)": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mỹ ngắn (12/31/2024)" + "US Short (12/31/2024)" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mỹ ngắn (12/31/2024)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "美式短格式 (12/31/2024)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "美式短格式 (12/31/2024)" } } } }, - "Use ~/.pgpass": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sử dụng ~/.pgpass" + "Use ~/.pgpass" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sử dụng ~/.pgpass" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "使用 ~/.pgpass" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 ~/.pgpass" } } } }, - "Use Default": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mặc định" + "Use Default" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mặc định" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "使用默认" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用默认" } } } }, - "Use SSL if available": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Sử dụng SSL nếu có" + "Use SSL if available" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sử dụng SSL nếu có" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "可用时使用 SSL" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "可用时使用 SSL" } } } }, - "User": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Người dùng" + "User" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Người dùng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "用户" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户" } } } }, - "User-installed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Người dùng cài đặt" + "User-installed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Người dùng cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "用户安装" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户安装" } } } }, - "username": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "username" + "username" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "username" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "username" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "username" } } } }, - "Username": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tên người dùng" + "Username" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tên người dùng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "用户名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用户名" } } } }, - "UTC_TIMESTAMP()": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "UTC_TIMESTAMP()" + "UTC_TIMESTAMP()" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "UTC_TIMESTAMP()" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "UTC_TIMESTAMP()" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "UTC_TIMESTAMP()" } } } }, - "v%@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "v%@" + "v%@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "v%@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "v%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "v%@" } } } }, - "v%@ · %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "v%1$@ · %2$@" + "v%@ · %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "v%1$@ · %2$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "v%@ · %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "v%@ · %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "v%@ · %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "v%@ · %@" } } } }, - "v%@+": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "v%@+" + "v%@+" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "v%@+" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "v%@+" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "v%@+" } } } }, - "Validation Failed": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực thất bại" + "Validation Failed" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác thực thất bại" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "验证失败" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证失败" } } } }, - "Value": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị" + "Value" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "值" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "值" } } } }, - "Value is required": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Giá trị là bắt buộc" + "Value is required" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giá trị là bắt buộc" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "值为必填项" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "值为必填项" } } } }, - "Verification Code Required": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Cần mã xác minh" + "Verification Code Required" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cần mã xác minh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "需要验证码" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要验证码" } } } }, - "Verified": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã xác minh" + "Verified" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã xác minh" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已验证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已验证" } } } }, - "Verified by TablePro": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Đã xác minh bởi TablePro" + "Verified by TablePro" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã xác minh bởi TablePro" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已通过 TablePro 验证" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已通过 TablePro 验证" } } } }, - "Verify certificate and hostname": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác minh chứng chỉ và tên máy chủ" + "Verify CA" : { + + }, + "Verify certificate and hostname" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác minh chứng chỉ và tên máy chủ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "验证证书和主机名" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证证书和主机名" } } } }, - "Verify server certificate": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác minh chứng chỉ máy chủ" + "Verify Identity" : { + + }, + "Verify server certificate" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xác minh chứng chỉ máy chủ" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "验证服务器证书" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证服务器证书" } } } }, - "Version": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phiên bản" + "Version" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phiên bản" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "版本" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本" } } } }, - "Version %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phiên bản %@" + "Version %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phiên bản %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "版本 %@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本 %@" } } } }, - "Version %@ (Build %@)": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "Version %1$@ (Build %2$@)" + "Version %@ (Build %@)" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Version %1$@ (Build %2$@)" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phiên bản %1$@ (Bản dựng %2$@)" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phiên bản %1$@ (Bản dựng %2$@)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "版本 %@(构建 %@)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本 %@(构建 %@)" } } } }, - "Version:": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Phiên bản:" + "Version:" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Phiên bản:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "版本:" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "版本:" } } } }, - "View": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xem" + "View" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "视图" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "视图" } } } }, - "View Mode": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ xem" + "View Mode" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ xem" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "视图模式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "视图模式" } } } }, - "View: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ xem: %@" + "View: %@" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ xem: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "视图:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "视图:%@" } } } }, - "Vim mode": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chế độ Vim" + "Vim mode" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chế độ Vim" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Vim 模式" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vim 模式" } } } }, - "Warning": {}, - "WARNING: Failed to re-enable foreign key checks: %@. Please manually verify FK constraints are enabled.": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "CẢNH BÁO: Bật lại kiểm tra khóa ngoại thất bại: %@. Vui lòng kiểm tra thủ công rằng ràng buộc FK đã được bật." + "Warning" : { + + }, + "WARNING: Failed to re-enable foreign key checks: %@. Please manually verify FK constraints are enabled." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CẢNH BÁO: Bật lại kiểm tra khóa ngoại thất bại: %@. Vui lòng kiểm tra thủ công rằng ràng buộc FK đã được bật." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "警告:重新启用外键检查失败:%@。请手动验证外键约束已启用。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "警告:重新启用外键检查失败:%@。请手动验证外键约束已启用。" } } } }, - "WARNING: The host key for '%@' has changed!\n\nThis could mean someone is doing something malicious, or the server was reinstalled.\n\nPrevious fingerprint: %@\nCurrent fingerprint: %@": { - "localizations": { - "en": { - "stringUnit": { - "state": "new", - "value": "WARNING: The host key for '%1$@' has changed!\n\nThis could mean someone is doing something malicious, or the server was reinstalled.\n\nPrevious fingerprint: %2$@\nCurrent fingerprint: %3$@" + "WARNING: The host key for '%@' has changed!\n\nThis could mean someone is doing something malicious, or the server was reinstalled.\n\nPrevious fingerprint: %@\nCurrent fingerprint: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "WARNING: The host key for '%1$@' has changed!\n\nThis could mean someone is doing something malicious, or the server was reinstalled.\n\nPrevious fingerprint: %2$@\nCurrent fingerprint: %3$@" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "CẢNH BÁO: Khóa máy chủ của '%@' đã thay đổi!\n\nĐiều này có thể do ai đó đang tấn công, hoặc máy chủ đã được cài đặt lại.\n\nVân tay trước: %@\nVân tay hiện tại: %@" + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CẢNH BÁO: Khóa máy chủ của '%@' đã thay đổi!\n\nĐiều này có thể do ai đó đang tấn công, hoặc máy chủ đã được cài đặt lại.\n\nVân tay trước: %@\nVân tay hiện tại: %@" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "警告:主机 '%@' 的密钥已更改!\n\n这可能意味着有人进行恶意操作,或者服务器已被重新安装。\n\n之前的指纹:%@\n当前指纹:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "警告:主机 '%@' 的密钥已更改!\n\n这可能意味着有人进行恶意操作,或者服务器已被重新安装。\n\n之前的指纹:%@\n当前指纹:%@" } } } }, - "Website": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Trang web" + "Website" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trang web" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "网站" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "网站" } } } }, - "Welcome to TablePro": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chào mừng đến với TablePro" + "Welcome to TablePro" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chào mừng đến với TablePro" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "欢迎使用 TablePro" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "欢迎使用 TablePro" } } } }, - "What you can do": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có thể làm gì" + "What you can do" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có thể làm gì" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "您可以做什么" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以做什么" } } } }, - "When enabled, clicking a new table replaces the current clean table tab instead of opening a new tab": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khi bật, nhấp vào bảng mới sẽ thay thế tab bảng trống hiện tại thay vì mở tab mới" + "When enabled, clicking a new table replaces the current clean table tab instead of opening a new tab" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khi bật, nhấp vào bảng mới sẽ thay thế tab bảng trống hiện tại thay vì mở tab mới" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启用后,点击新表将替换当前空白表标签页,而不是打开新标签页" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用后,点击新表将替换当前空白表标签页,而不是打开新标签页" } } } }, - "When enabled, clicking a table in the sidebar will replace the current tab if it has no unsaved changes and you haven't interacted with it (sorted, filtered, etc.).": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khi bật, nhấp vào bảng trong thanh bên sẽ thay thế tab hiện tại nếu không có thay đổi chưa lưu và bạn chưa tương tác với nó (sắp xếp, lọc, v.v.)." + "When enabled, clicking a table in the sidebar will replace the current tab if it has no unsaved changes and you haven't interacted with it (sorted, filtered, etc.)." : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khi bật, nhấp vào bảng trong thanh bên sẽ thay thế tab hiện tại nếu không có thay đổi chưa lưu và bạn chưa tương tác với nó (sắp xếp, lọc, v.v.)." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "启用后,点击侧边栏中的表将替换当前标签页(如果没有未保存的更改且您未与之交互过,如排序、筛选等)。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用后,点击侧边栏中的表将替换当前标签页(如果没有未保存的更改且您未与之交互过,如排序、筛选等)。" } } } }, - "When enabled, this favorite is visible in all connections": {}, - "When TablePro starts:": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Khi TablePro khởi động:" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "TablePro 启动时:" - } - } - } + "When enabled, this favorite is visible in all connections" : { + }, - "WHERE clause...": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Mệnh đề WHERE..." + "When TablePro starts:" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khi TablePro khởi động:" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "WHERE 子句..." + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "TablePro 启动时:" } } } }, - "Window Background": {}, - "With Headers": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Kèm tiêu đề" + "WHERE clause..." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mệnh đề WHERE..." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "含表头" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "WHERE 子句..." } } } }, - "Word wrap": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Tự động xuống dòng" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "自动换行" - } - } - } + "Window Background" : { + }, - "Wrap in transaction (BEGIN/COMMIT)": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bọc trong giao dịch (BEGIN/COMMIT)" + "With Headers" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kèm tiêu đề" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "包裹在事务中 (BEGIN/COMMIT)" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "含表头" } } } }, - "Write Concern": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Write Concern" + "Word wrap" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động xuống dòng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "Write Concern" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动换行" } } } }, - "Yellow": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Vàng" + "Wrap in transaction (BEGIN/COMMIT)" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bọc trong giao dịch (BEGIN/COMMIT)" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "黄色" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "包裹在事务中 (BEGIN/COMMIT)" } } } }, - "You": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn" + "Write Concern" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Write Concern" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "你" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Write Concern" } } } }, - "You can re-enable this in Settings": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có thể bật lại trong Cài đặt" + "Yellow" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vàng" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "您可以在设置中重新启用" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "黄色" } } } }, - "You have unsaved changes to the table structure. Refreshing will discard these changes.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn có thay đổi chưa lưu trong cấu trúc bảng. Làm mới sẽ hủy các thay đổi này." + "You" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "您有未保存的表结构更改。刷新将丢弃这些更改。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你" } } } }, - "You will be prompted for a verification code each time you connect.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn sẽ được yêu cầu nhập mã xác minh mỗi lần kết nối." + "You can re-enable this in Settings" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có thể bật lại trong Cài đặt" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每次连接时都会要求您输入验证码。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您可以在设置中重新启用" } } } }, - "You're all set!": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Bạn đã sẵn sàng!" + "You have unsaved changes to the table structure. Refreshing will discard these changes." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có thay đổi chưa lưu trong cấu trúc bảng. Làm mới sẽ hủy các thay đổi này." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "一切就绪!" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您有未保存的表结构更改。刷新将丢弃这些更改。" } } } }, - "Your changes will be lost if you don't save them.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Thay đổi của bạn sẽ bị mất nếu không lưu." + "You will be prompted for a verification code each time you connect." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn sẽ được yêu cầu nhập mã xác minh mỗi lần kết nối." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "如果不保存,您的更改将会丢失。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每次连接时都会要求您输入验证码。" } } } }, - "Your database schema and query data will be sent to the AI provider for analysis. Allow for this connection?": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Lược đồ cơ sở dữ liệu và dữ liệu truy vấn sẽ được gửi đến nhà cung cấp AI để phân tích. Cho phép kết nối này?" + "You're all set!" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn đã sẵn sàng!" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "您的数据库结构和查询数据将被发送给 AI 提供商进行分析。是否允许此连接?" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "一切就绪!" } } } }, - "Your executed queries will\nappear here for quick access.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Các truy vấn đã thực thi sẽ\nxuất hiện ở đây để truy cập nhanh." + "Your changes will be lost if you don't save them." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thay đổi của bạn sẽ bị mất nếu không lưu." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已执行的查询将\n显示在此处以便快速访问。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "如果不保存,您的更改将会丢失。" } } } }, - "Your license has expired": {}, - "Zero Fill": { - "extractionState": "stale", - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Điền số 0" + "Your database schema and query data will be sent to the AI provider for analysis. Allow for this connection?" : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lược đồ cơ sở dữ liệu và dữ liệu truy vấn sẽ được gửi đến nhà cung cấp AI để phân tích. Cho phép kết nối này?" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "零填充" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您的数据库结构和查询数据将被发送给 AI 提供商进行分析。是否允许此连接?" } } } }, - "Navigating pages will reload data and discard all unsaved changes.": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Chuyển trang sẽ tải lại dữ liệu và hủy tất cả thay đổi chưa lưu." + "Your executed queries will\nappear here for quick access." : { + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Các truy vấn đã thực thi sẽ\nxuất hiện ở đây để truy cập nhanh." } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "翻页将重新加载数据并丢弃所有未保存的更改。" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已执行的查询将\n显示在此处以便快速访问。" } } } }, - "Public key authentication failed: %@": { - "localizations": { - "vi": { - "stringUnit": { - "state": "translated", - "value": "Xác thực khóa công khai thất bại: %@" + "Your license has expired" : { + + }, + "Zero Fill" : { + "extractionState" : "stale", + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Điền số 0" } }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "公钥认证失败:%@" + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "零填充" } } } } }, - "version": "1.0" -} + "version" : "1.0" +} \ No newline at end of file From 027fc440c6aecb3391bed7d0b8501d0b95e4469f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 19:52:23 +0700 Subject: [PATCH 2/4] fix: address PR review for etcd plugin (critical + major issues) --- .../EtcdDriverPlugin/EtcdCommandParser.swift | 51 +++++++++++++++++-- Plugins/EtcdDriverPlugin/EtcdHttpClient.swift | 31 +++++++++-- .../EtcdDriverPlugin/EtcdPluginDriver.swift | 8 ++- .../EtcdDriverPlugin/EtcdQueryBuilder.swift | 14 ++++- .../EtcdStatementGenerator.swift | 9 +++- TablePro.xcodeproj/project.pbxproj | 6 +-- 6 files changed, 101 insertions(+), 18 deletions(-) diff --git a/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift b/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift index 2a3dfe90..fa0c0bf5 100644 --- a/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift +++ b/Plugins/EtcdDriverPlugin/EtcdCommandParser.swift @@ -424,24 +424,50 @@ struct EtcdCommandParser { var inQuote = false var quoteChar: Character = "\"" var escapeNext = false + var tokenStarted = false for char in input { if escapeNext { - current.append(char) + tokenStarted = true + if inQuote { + switch char { + case "n": current.append("\n") + case "r": current.append("\r") + case "t": current.append("\t") + case "\\": current.append("\\") + case "\"": current.append("\"") + case "'": current.append("'") + default: + current.append("\\") + current.append(char) + } + } else { + // Outside quotes, preserve literal backslash + current.append("\\") + current.append(char) + } escapeNext = false continue } if char == "\\" { - escapeNext = true + if inQuote { + escapeNext = true + } else { + // Outside quotes, backslash is literal + current.append(char) + tokenStarted = true + } continue } if inQuote { if char == quoteChar { inQuote = false + tokenStarted = true // preserve empty quoted token } else { current.append(char) + tokenStarted = true } continue } @@ -449,21 +475,29 @@ struct EtcdCommandParser { if char == "\"" || char == "'" { inQuote = true quoteChar = char + tokenStarted = true continue } if char.isWhitespace { - if !current.isEmpty { + if tokenStarted { tokens.append(current) current = "" + tokenStarted = false } continue } current.append(char) + tokenStarted = true + } + + if escapeNext { + current.append("\\") + tokenStarted = true } - if !current.isEmpty { + if tokenStarted { tokens.append(current) } @@ -479,20 +513,27 @@ private struct ParsedFlags { mutating func parse(from tokens: [String]) -> [String] { var positional: [String] = [] + var index = 0 - for token in tokens { + while index < tokens.count { + let token = tokens[index] if token.hasPrefix("--") { let flagContent = String(token.dropFirst(2)) if let equalsIndex = flagContent.firstIndex(of: "=") { let key = String(flagContent[flagContent.startIndex..(path: String, body: Req) async throws -> Data { + private func performRequest(path: String, body: Req, allowReauth: Bool = true) async throws -> Data { lock.lock() guard let session else { lock.unlock() @@ -663,9 +672,9 @@ final class EtcdHttpClient: @unchecked Sendable { let alreadyAuthenticating = _isAuthenticating lock.unlock() - if !alreadyAuthenticating, !config.username.isEmpty { + if allowReauth, !alreadyAuthenticating, !config.username.isEmpty { try await authenticate() - return try await performRequest(path: path, body: body) + return try await performRequest(path: path, body: body, allowReauth: false) } let errorBody = String(data: data, encoding: .utf8) ?? "Unauthorized" throw EtcdError.authFailed(errorBody) @@ -1036,6 +1045,20 @@ final class EtcdHttpClient: @unchecked Sendable { var identity: SecIdentity? let status = SecIdentityCreateWithCertificate(nil, certificate, &identity) + + // Clean up: remove imported items from keychain + let deleteCertQuery: [String: Any] = [ + kSecClass as String: kSecClassCertificate, + kSecValueRef as String: certificate + ] + SecItemDelete(deleteCertQuery as CFDictionary) + + let deleteKeyQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecValueRef as String: privateKey + ] + SecItemDelete(deleteKeyQuery as CFDictionary) + if status == errSecSuccess { return identity } diff --git a/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift b/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift index 775ede7f..296451be 100644 --- a/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift +++ b/Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift @@ -34,6 +34,10 @@ final class EtcdPluginDriver: PluginDatabaseDriver, @unchecked Sendable { var supportsTransactions: Bool { false } + func beginTransaction() async throws {} + func commitTransaction() async throws {} + func rollbackTransaction() async throws {} + func quoteIdentifier(_ name: String) -> String { name } func defaultExportQuery(table: String) -> String? { @@ -271,7 +275,7 @@ final class EtcdPluginDriver: PluginDatabaseDriver, @unchecked Sendable { return """ // etcd key prefix: \(prefix.isEmpty ? "(all keys)" : prefix) // Keys: \(count) - // Use 'get \(prefix.isEmpty ? "/" : prefix) --prefix' to browse keys + // Use 'get \(prefix.isEmpty ? "\"\"" : prefix) --prefix' to browse keys """ } @@ -391,7 +395,7 @@ final class EtcdPluginDriver: PluginDatabaseDriver, @unchecked Sendable { func allTablesMetadataSQL(schema: String?) -> String? { let prefix = _rootPrefix - return "get \(escapeArgument(prefix.isEmpty ? "/" : prefix)) --prefix --keys-only" + return "get \(escapeArgument(prefix)) --prefix --keys-only" } // MARK: - Command Dispatch diff --git a/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift b/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift index ce4d9a93..25390f66 100644 --- a/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift +++ b/Plugins/EtcdDriverPlugin/EtcdQueryBuilder.swift @@ -55,7 +55,8 @@ struct EtcdQueryBuilder { sortColumns: [(columnIndex: Int, ascending: Bool)], limit: Int, offset: Int - ) -> String { + ) -> String? { + if hasUnsupportedFilters(filters) { return nil } let sortAsc = sortColumns.first?.ascending ?? true let (filterType, filterValue) = extractKeyFilter(from: filters) return Self.encodeRangeQuery( @@ -86,7 +87,8 @@ struct EtcdQueryBuilder { sortColumns: [(columnIndex: Int, ascending: Bool)], limit: Int, offset: Int - ) -> String { + ) -> String? { + if hasUnsupportedFilters(filters) { return nil } let sortAsc = sortColumns.first?.ascending ?? true if !searchText.isEmpty { return Self.encodeRangeQuery( @@ -185,6 +187,14 @@ struct EtcdQueryBuilder { // MARK: - Filter Extraction + /// Returns true if any filter targets a column other than "Key", + /// which etcd cannot handle server-side (Value, Lease, Version, etc.). + private func hasUnsupportedFilters( + _ filters: [(column: String, op: String, value: String)] + ) -> Bool { + filters.contains { $0.column != "Key" } + } + private func extractKeyFilter( from filters: [(column: String, op: String, value: String)] ) -> (EtcdFilterType, String) { diff --git a/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift b/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift index 1a91bd72..11ac2879 100644 --- a/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift +++ b/Plugins/EtcdDriverPlugin/EtcdStatementGenerator.swift @@ -96,10 +96,12 @@ struct EtcdStatementGenerator { let keyChange = change.cellChanges.first { $0.columnName == "Key" } let newKey = keyChange?.newValue ?? originalKey - if newKey != originalKey { - statements.append((statement: "del \(escapeArgument(originalKey))", parameters: [])) + guard !newKey.isEmpty else { + Self.logger.warning("Skipping UPDATE - empty key") + return [] } + let shouldDeleteOriginalKey = newKey != originalKey let valueChange = change.cellChanges.first { $0.columnName == "Value" } let leaseChange = change.cellChanges.first { $0.columnName == "Lease" } @@ -110,6 +112,9 @@ struct EtcdStatementGenerator { cmd += " --lease=\(lease)" } statements.append((statement: cmd, parameters: [])) + if shouldDeleteOriginalKey { + statements.append((statement: "del \(escapeArgument(originalKey))", parameters: [])) + } } else if let lease = leaseChange?.newValue { let currentValue = extractOriginalValue(from: change) ?? "" var cmd = "put \(escapeArgument(newKey)) \(escapeArgument(currentValue))" diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index 7a32062a..5d0ce5ee 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -716,7 +716,7 @@ 5AEA8B3F2F6808CA0040461A /* EtcdQueryBuilder.swift */, 5AEA8B402F6808CA0040461A /* EtcdStatementGenerator.swift */, ); - path = EtcdDriverPlugin; + path = Plugins/EtcdDriverPlugin; sourceTree = ""; }; 5AEA8B482F6808E90040461A /* Frameworks */ = { @@ -2918,7 +2918,7 @@ SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 5.9; WRAPPER_EXTENSION = tableplugin; }; name = Debug; @@ -2942,7 +2942,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 5.9; WRAPPER_EXTENSION = tableplugin; }; name = Release; From 610ea518a19aa384f5a654dea9469842d6d22f6f Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 20:09:24 +0700 Subject: [PATCH 3/4] fix: generate WHERE clauses for SQL database filters and quick search (#341) --- .../Core/Database/FilterSQLGenerator.swift | 21 ++++++ .../Services/Query/TableQueryBuilder.swift | 74 ++++++++++++++++++- .../MainContentCoordinator+Pagination.swift | 16 +--- .../Views/Main/MainContentCoordinator.swift | 6 +- 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/TablePro/Core/Database/FilterSQLGenerator.swift b/TablePro/Core/Database/FilterSQLGenerator.swift index 64e30a65..91f0294d 100644 --- a/TablePro/Core/Database/FilterSQLGenerator.swift +++ b/TablePro/Core/Database/FilterSQLGenerator.swift @@ -38,6 +38,27 @@ struct FilterSQLGenerator { return conditions.joined(separator: separator) } + /// Generate WHERE clause for quick search across multiple columns + func generateQuickSearchWhereClause(searchText: String, columns: [String]) -> String { + let conditions = generateQuickSearchConditions(searchText: searchText, columns: columns) + guard !conditions.isEmpty else { return "" } + return "WHERE (\(conditions))" + } + + /// Generate OR-joined LIKE conditions for quick search (without WHERE keyword) + func generateQuickSearchConditions(searchText: String, columns: [String]) -> String { + guard !searchText.isEmpty, !columns.isEmpty else { return "" } + let escapedValue = escapeLikeWildcards(searchText) + let pattern = "%\(escapedValue)%" + let quotedPattern = escapeSQLQuote(pattern) + let escape = likeEscapeClause + let conditions = columns.map { column in + let quoted = quoteIdentifierFn(column) + return "\(quoted) LIKE '\(quotedPattern)'\(escape)" + } + return conditions.joined(separator: " OR ") + } + /// Generate a single filter condition func generateCondition(from filter: TableFilter) -> String? { guard filter.isValid else { return nil } diff --git a/TablePro/Core/Services/Query/TableQueryBuilder.swift b/TablePro/Core/Services/Query/TableQueryBuilder.swift index 725037e5..14e5f863 100644 --- a/TablePro/Core/Services/Query/TableQueryBuilder.swift +++ b/TablePro/Core/Services/Query/TableQueryBuilder.swift @@ -15,6 +15,7 @@ struct TableQueryBuilder { private let databaseType: DatabaseType private var pluginDriver: (any PluginDatabaseDriver)? + private let dialect: SQLDialectDescriptor? private let dialectQuote: (String) -> String // MARK: - Initialization @@ -22,10 +23,12 @@ struct TableQueryBuilder { init( databaseType: DatabaseType, pluginDriver: (any PluginDatabaseDriver)? = nil, + dialect: SQLDialectDescriptor? = nil, dialectQuote: ((String) -> String)? = nil ) { self.databaseType = databaseType self.pluginDriver = pluginDriver + self.dialect = dialect self.dialectQuote = dialectQuote ?? { name in let escaped = name.replacingOccurrences(of: "\"", with: "\"\"") return "\"\(escaped)\"" @@ -69,7 +72,7 @@ struct TableQueryBuilder { query += " \(orderBy)" } - query += " LIMIT \(limit) OFFSET \(offset)" + query += " \(buildPaginationClause(limit: limit, offset: offset))" return query } @@ -97,7 +100,22 @@ struct TableQueryBuilder { } let quotedTable = quote(tableName) - return "SELECT * FROM \(quotedTable) LIMIT \(limit) OFFSET \(offset)" + var query = "SELECT * FROM \(quotedTable)" + + if let dialect { + let filterGen = FilterSQLGenerator(dialect: dialect, quoteIdentifier: dialectQuote) + let whereClause = filterGen.generateWhereClause(from: filters, logicMode: logicMode) + if !whereClause.isEmpty { + query += " \(whereClause)" + } + } + + if let orderBy = buildOrderByClause(sortState: sortState, columns: columns) { + query += " \(orderBy)" + } + + query += " \(buildPaginationClause(limit: limit, offset: offset))" + return query } func buildQuickSearchQuery( @@ -119,7 +137,22 @@ struct TableQueryBuilder { } let quotedTable = quote(tableName) - return "SELECT * FROM \(quotedTable) LIMIT \(limit) OFFSET \(offset)" + var query = "SELECT * FROM \(quotedTable)" + + if let dialect { + let filterGen = FilterSQLGenerator(dialect: dialect, quoteIdentifier: dialectQuote) + let searchWhere = filterGen.generateQuickSearchWhereClause(searchText: searchText, columns: columns) + if !searchWhere.isEmpty { + query += " \(searchWhere)" + } + } + + if let orderBy = buildOrderByClause(sortState: sortState, columns: columns) { + query += " \(orderBy)" + } + + query += " \(buildPaginationClause(limit: limit, offset: offset))" + return query } func buildCombinedQuery( @@ -149,7 +182,33 @@ struct TableQueryBuilder { } let quotedTable = quote(tableName) - return "SELECT * FROM \(quotedTable) LIMIT \(limit) OFFSET \(offset)" + var query = "SELECT * FROM \(quotedTable)" + + if let dialect { + let filterGen = FilterSQLGenerator(dialect: dialect, quoteIdentifier: dialectQuote) + var whereParts: [String] = [] + + let filterConditions = filterGen.generateConditions(from: filters, logicMode: logicMode) + if !filterConditions.isEmpty { + whereParts.append("(\(filterConditions))") + } + + let searchConditions = filterGen.generateQuickSearchConditions(searchText: searchText, columns: searchColumns) + if !searchConditions.isEmpty { + whereParts.append("(\(searchConditions))") + } + + if !whereParts.isEmpty { + query += " WHERE \(whereParts.joined(separator: " AND "))" + } + } + + if let orderBy = buildOrderByClause(sortState: sortState, columns: columns) { + query += " \(orderBy)" + } + + query += " \(buildPaginationClause(limit: limit, offset: offset))" + return query } func buildSortedQuery( @@ -213,6 +272,13 @@ struct TableQueryBuilder { // MARK: - Private Helpers + private func buildPaginationClause(limit: Int, offset: Int) -> String { + if let dialect, dialect.paginationStyle == .offsetFetch { + return "OFFSET \(offset) ROWS FETCH NEXT \(limit) ROWS ONLY" + } + return "LIMIT \(limit) OFFSET \(offset)" + } + private func sortColumnsAsTuples(_ sortState: SortState?) -> [(columnIndex: Int, ascending: Bool)] { sortState?.columns.compactMap { sortCol -> (columnIndex: Int, ascending: Bool)? in guard sortCol.columnIndex >= 0 else { return nil } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift index 8bcb9e20..a8e5c737 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift @@ -100,24 +100,12 @@ extension MainContentCoordinator { } } - /// Reload current page data private func reloadCurrentPage() { guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count, - let tableName = tabManager.tabs[tabIndex].tableName else { return } + tabManager.tabs[tabIndex].tableName != nil else { return } - let tab = tabManager.tabs[tabIndex] - let pagination = tab.pagination - - let newQuery = queryBuilder.buildBaseQuery( - tableName: tableName, - sortState: tab.sortState, - columns: tab.resultColumns, - limit: pagination.pageSize, - offset: pagination.currentOffset - ) - - tabManager.tabs[tabIndex].query = newQuery + rebuildTableQuery(at: tabIndex) runQuery() } } diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 53a50960..c6dcb451 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -232,11 +232,11 @@ final class MainContentCoordinator { self.filterStateManager = filterStateManager self.columnVisibilityManager = columnVisibilityManager self.toolbarState = toolbarState + let dialect = PluginManager.shared.sqlDialect(for: connection.type) self.queryBuilder = TableQueryBuilder( databaseType: connection.type, - dialectQuote: quoteIdentifierFromDialect( - PluginManager.shared.sqlDialect(for: connection.type) - ) + dialect: dialect, + dialectQuote: quoteIdentifierFromDialect(dialect) ) self.persistence = TabPersistenceCoordinator(connectionId: connection.id) From fcc2d8af53a4bf86354956dbd2f609b783d2d60c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 16 Mar 2026 20:44:14 +0700 Subject: [PATCH 4/4] fix: filter disabled rows, cast PostgreSQL quick search to text, add tests --- .../Core/Database/FilterSQLGenerator.swift | 5 +- .../Services/Query/TableQueryBuilder.swift | 6 +- .../MainContentCoordinator+Pagination.swift | 3 +- .../TableQueryBuilderFilterTests.swift | 184 ++++++++++++++++++ 4 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 TableProTests/Core/Services/TableQueryBuilderFilterTests.swift diff --git a/TablePro/Core/Database/FilterSQLGenerator.swift b/TablePro/Core/Database/FilterSQLGenerator.swift index 91f0294d..85fca247 100644 --- a/TablePro/Core/Database/FilterSQLGenerator.swift +++ b/TablePro/Core/Database/FilterSQLGenerator.swift @@ -52,9 +52,12 @@ struct FilterSQLGenerator { let pattern = "%\(escapedValue)%" let quotedPattern = escapeSQLQuote(pattern) let escape = likeEscapeClause + // CAST to TEXT for databases like PostgreSQL where LIKE on non-text columns fails + let needsCast = dialect.regexSyntax == .tilde let conditions = columns.map { column in let quoted = quoteIdentifierFn(column) - return "\(quoted) LIKE '\(quotedPattern)'\(escape)" + let target = needsCast ? "CAST(\(quoted) AS TEXT)" : quoted + return "\(target) LIKE '\(quotedPattern)'\(escape)" } return conditions.joined(separator: " OR ") } diff --git a/TablePro/Core/Services/Query/TableQueryBuilder.swift b/TablePro/Core/Services/Query/TableQueryBuilder.swift index 14e5f863..2f8e7f28 100644 --- a/TablePro/Core/Services/Query/TableQueryBuilder.swift +++ b/TablePro/Core/Services/Query/TableQueryBuilder.swift @@ -103,8 +103,9 @@ struct TableQueryBuilder { var query = "SELECT * FROM \(quotedTable)" if let dialect { + let activeFilters = filters.filter { $0.isEnabled } let filterGen = FilterSQLGenerator(dialect: dialect, quoteIdentifier: dialectQuote) - let whereClause = filterGen.generateWhereClause(from: filters, logicMode: logicMode) + let whereClause = filterGen.generateWhereClause(from: activeFilters, logicMode: logicMode) if !whereClause.isEmpty { query += " \(whereClause)" } @@ -185,10 +186,11 @@ struct TableQueryBuilder { var query = "SELECT * FROM \(quotedTable)" if let dialect { + let activeFilters = filters.filter { $0.isEnabled } let filterGen = FilterSQLGenerator(dialect: dialect, quoteIdentifier: dialectQuote) var whereParts: [String] = [] - let filterConditions = filterGen.generateConditions(from: filters, logicMode: logicMode) + let filterConditions = filterGen.generateConditions(from: activeFilters, logicMode: logicMode) if !filterConditions.isEmpty { whereParts.append("(\(filterConditions))") } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift index a8e5c737..3f8ec616 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift @@ -102,8 +102,7 @@ extension MainContentCoordinator { private func reloadCurrentPage() { guard let tabIndex = tabManager.selectedTabIndex, - tabIndex < tabManager.tabs.count, - tabManager.tabs[tabIndex].tableName != nil else { return } + tabIndex < tabManager.tabs.count else { return } rebuildTableQuery(at: tabIndex) runQuery() diff --git a/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift b/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift new file mode 100644 index 00000000..80666675 --- /dev/null +++ b/TableProTests/Core/Services/TableQueryBuilderFilterTests.swift @@ -0,0 +1,184 @@ +// +// TableQueryBuilderFilterTests.swift +// TableProTests +// +// Tests for TableQueryBuilder WHERE clause generation in fallback paths. +// + +import Foundation +@testable import TablePro +import Testing + +@Suite("Table Query Builder - Filtered Query Fallback") +struct TableQueryBuilderFilteredQueryTests { + private let builder = TableQueryBuilder(databaseType: .mysql) + + @Test("buildFilteredQuery with enabled filter produces WHERE clause") + func filteredQueryWithEnabledFilter() { + var filter = TableFilter() + filter.columnName = "name" + filter.filterOperator = .equal + filter.value = "Alice" + filter.isEnabled = true + + let query = builder.buildFilteredQuery( + tableName: "users", filters: [filter] + ) + #expect(query.contains("WHERE")) + #expect(query.contains("name")) + #expect(query.contains("Alice")) + } + + @Test("buildFilteredQuery excludes disabled filters") + func filteredQueryExcludesDisabledFilter() { + var enabledFilter = TableFilter() + enabledFilter.columnName = "name" + enabledFilter.filterOperator = .equal + enabledFilter.value = "Alice" + enabledFilter.isEnabled = true + + var disabledFilter = TableFilter() + disabledFilter.columnName = "age" + disabledFilter.filterOperator = .equal + disabledFilter.value = "30" + disabledFilter.isEnabled = false + + let query = builder.buildFilteredQuery( + tableName: "users", filters: [enabledFilter, disabledFilter] + ) + #expect(query.contains("name")) + #expect(!query.contains("age")) + } + + @Test("buildFilteredQuery with no enabled filters produces no WHERE") + func filteredQueryNoEnabledFilters() { + var filter = TableFilter() + filter.columnName = "name" + filter.filterOperator = .equal + filter.value = "Alice" + filter.isEnabled = false + + let query = builder.buildFilteredQuery( + tableName: "users", filters: [filter] + ) + #expect(!query.contains("WHERE")) + } + + @Test("buildFilteredQuery with empty filters produces no WHERE") + func filteredQueryEmptyFilters() { + let query = builder.buildFilteredQuery( + tableName: "users", filters: [] + ) + #expect(!query.contains("WHERE")) + #expect(query.contains("SELECT * FROM")) + } +} + +@Suite("Table Query Builder - Quick Search Fallback") +struct TableQueryBuilderQuickSearchTests { + private let builder = TableQueryBuilder(databaseType: .mysql) + + @Test("buildQuickSearchQuery produces OR-joined LIKE conditions") + func quickSearchProducesLike() { + let query = builder.buildQuickSearchQuery( + tableName: "users", searchText: "alice", + columns: ["name", "email"] + ) + #expect(query.contains("WHERE")) + #expect(query.contains("LIKE")) + #expect(query.contains("alice")) + } + + @Test("buildQuickSearchQuery with empty search text produces no WHERE") + func quickSearchEmptyText() { + let query = builder.buildQuickSearchQuery( + tableName: "users", searchText: "", + columns: ["name", "email"] + ) + #expect(!query.contains("WHERE")) + } +} + +@Suite("Table Query Builder - Combined Query Fallback") +struct TableQueryBuilderCombinedQueryTests { + private let builder = TableQueryBuilder(databaseType: .mysql) + + @Test("buildCombinedQuery with filter and search produces both in WHERE") + func combinedQueryFilterAndSearch() { + var filter = TableFilter() + filter.columnName = "status" + filter.filterOperator = .equal + filter.value = "active" + filter.isEnabled = true + + let query = builder.buildCombinedQuery( + tableName: "users", filters: [filter], + searchText: "alice", searchColumns: ["name", "email"] + ) + #expect(query.contains("WHERE")) + #expect(query.contains("status")) + #expect(query.contains("LIKE")) + #expect(query.contains("AND")) + } + + @Test("buildCombinedQuery excludes disabled filters") + func combinedQueryExcludesDisabledFilter() { + var disabledFilter = TableFilter() + disabledFilter.columnName = "age" + disabledFilter.filterOperator = .equal + disabledFilter.value = "30" + disabledFilter.isEnabled = false + + let query = builder.buildCombinedQuery( + tableName: "users", filters: [disabledFilter], + searchText: "alice", searchColumns: ["name"] + ) + #expect(!query.contains("age")) + #expect(query.contains("LIKE")) + } +} + +@Suite("Table Query Builder - PostgreSQL Quick Search CAST") +struct TableQueryBuilderPostgreSQLQuickSearchTests { + private let builder = TableQueryBuilder(databaseType: .postgresql) + + @Test("PostgreSQL quick search uses CAST for LIKE on non-text columns") + func postgresQuickSearchCast() { + let query = builder.buildQuickSearchQuery( + tableName: "users", searchText: "test", + columns: ["id", "name"] + ) + #expect(query.contains("CAST(")) + #expect(query.contains("AS TEXT)")) + #expect(query.contains("LIKE")) + } +} + +@Suite("Table Query Builder - NoSQL Nil Dialect Fallback") +struct TableQueryBuilderNoSQLTests { + // MongoDB has no SQL dialect — should produce bare SELECT without WHERE + private let builder = TableQueryBuilder(databaseType: .mongodb) + + @Test("NoSQL type produces no WHERE for filtered query") + func noSqlFilteredQueryNoWhere() { + var filter = TableFilter() + filter.columnName = "name" + filter.filterOperator = .equal + filter.value = "Alice" + filter.isEnabled = true + + let query = builder.buildFilteredQuery( + tableName: "collection", filters: [filter] + ) + #expect(!query.contains("WHERE")) + } + + @Test("NoSQL type produces no WHERE for quick search") + func noSqlQuickSearchNoWhere() { + let query = builder.buildQuickSearchQuery( + tableName: "collection", searchText: "test", + columns: ["field1"] + ) + #expect(!query.contains("WHERE")) + } +}