diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acc4cd50..64146a07f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ENUM/SET column editor: double-click ENUM columns to select from a searchable dropdown popover, SET columns show a multi-select checkbox popover with OK/Cancel buttons - PostgreSQL user-defined enum type support via `pg_enum` catalog lookup - SQLite CHECK constraint pseudo-enum detection (e.g., `CHECK(col IN ('a','b','c'))`) +- Language setting in General preferences (System, English, Vietnamese) with full Vietnamese localization (637 strings) ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 6e517c36c..641c66ea4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -487,6 +487,7 @@ mint dev - **Never** use tabs for indentation (except Makefile/pbxproj) - **Always** run `swiftlint lint --strict` after making changes to verify compliance +- **Always** use `String(localized:)` for new user-facing strings instead of hardcoding English text. The project uses Xcode String Catalogs (`Localizable.xcstrings`) for localization. SwiftUI view literals (`Text("literal")`, `Button("literal")`, etc.) auto-localize, but computed `String` properties, AppKit code (`NSMenuItem`, `.title`), alert messages, and error descriptions must use `String(localized: "text")`. Do NOT localize technical terms (font names, database types, SQL keywords, encoding names, format patterns). - **Always** update `CHANGELOG.md` when adding features, fixing bugs, or making notable changes. Add entries under the `[Unreleased]` section using the existing format (Added/Fixed/Changed subsections). This is **mandatory** — do not skip it. - **Always** update documentation when adding or changing features — this is a **mandatory** step, not optional. After implementing a feature, check the "When to Update Documentation" table above and update both English (`docs/`) and Vietnamese (`docs/vi/`) pages. Key docs to check: - New keyboard shortcuts → `features/keyboard-shortcuts.mdx` diff --git a/TRACKING.md b/TRACKING.md index 332d1ee19..fc325ac68 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -15,9 +15,9 @@ | API Backend Security | 8/10 | Rate limiting, input validation, atomic locking added; still missing RBAC | | Documentation | 8/10 | Comprehensive, missing v0.2.0 changelog | | Accessibility | 2/10 | Only 2 a11y labels | -| Localization | 0/10 | English only, no i18n | +| Localization | 7/10 | English + Vietnamese (637 strings), language setting in General preferences | | Performance | 9/10 | Sophisticated optimizations | -| Dependencies | 9/10 | Minimal, well-maintained | +| Dependencies | 9/10 | Minimal, well-maintained; CodeEditSourceEditor tracks `main` branch (pending 0.16.0 release) | --- @@ -65,6 +65,11 @@ - **File:** `tablepro.app/docs/changelog.mdx` + `tablepro.app/docs/vi/changelog.mdx` - **Resolution:** Added v0.2.0 entry to both English and Vietnamese changelog pages (11 features, 7 fixes, 1 improvement) +### ~~C6. macOS 13 Launch Crash (asyncAndWait symbol missing)~~ DONE +- **Impact:** App crashes at launch on macOS 13 with `Symbol not found: _$sSo17OS_dispatch_queueC8DispatchE12asyncAndWait` +- **Root cause:** CodeEditSourceEditor 0.15.2 calls `DispatchQueue.main.asyncAndWait(execute:)` which requires macOS 14+; app targets macOS 13.5 +- **Resolution:** Updated CodeEditSourceEditor SPM dependency from version `0.15.2` to tracking `main` branch (commit `1fa4d3c`), which replaces `asyncAndWait` with `sync` + --- ## WARNING Issues @@ -260,11 +265,12 @@ The following v0.2.0 features are documented on feature pages but missing from c ## Technical Debt -### No Localization (i18n) -- No `.strings` files or String Catalogs -- All UI text hardcoded in English -- Competitors support 10-20+ languages -- Effort: Large (~2000+ strings to extract) +### ~~No Localization (i18n)~~ DONE +- String Catalog (`Localizable.xcstrings`) with 637 strings +- Full Vietnamese translation (100% coverage) +- Language setting in General preferences (System, English, Vietnamese) +- Requires app restart for language change to take effect +- Remaining: Add more languages (competitors support 10-20+) ### Minimal Accessibility - Only 2 `accessibilityLabel` instances in entire codebase @@ -300,7 +306,7 @@ The following v0.2.0 features are documented on feature pages but missing from c | Custom Themes | System only | Full | **Partial** | | SSH Tunneling | Full | Full | — | | Read-only Mode | v0.2.0 | Yes | — | -| Localization | English only | 10+ langs | **Missing** | +| Localization | English + Vietnamese | 10+ langs | **Partial** | | Cost | Free (GPL v3) | $99 | **Win** | ### TablePro Advantages @@ -333,7 +339,7 @@ The following v0.2.0 features are documented on feature pages but missing from c - [ ] ER diagram visualization - [ ] Keyboard shortcut customization - [ ] Connection health monitoring + auto-reconnect -- [ ] Localization infrastructure +- [x] Localization infrastructure ### Immediate Actions (This Week) 1. Update docs changelog with v0.2.0 diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index 8620d980e..3a7cd536a 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + vi, Base, ); mainGroup = 5A1091BE2EF17EDC0055EA7C; diff --git a/TablePro/AppDelegate.swift b/TablePro/AppDelegate.swift index ca5696b8e..0266924df 100644 --- a/TablePro/AppDelegate.swift +++ b/TablePro/AppDelegate.swift @@ -39,7 +39,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let menu = NSMenu() let welcomeItem = NSMenuItem( - title: "Show Welcome Window", + title: String(localized: "Show Welcome Window"), action: #selector(showWelcomeFromDock), keyEquivalent: "" ) @@ -49,7 +49,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Add connections submenu let connections = ConnectionStorage.shared.loadConnections() if !connections.isEmpty { - let connectionsItem = NSMenuItem(title: "Open Connection", action: nil, keyEquivalent: "") + let connectionsItem = NSMenuItem(title: String(localized: "Open Connection"), action: nil, keyEquivalent: "") let submenu = NSMenu() for connection in connections { diff --git a/TablePro/ContentView.swift b/TablePro/ContentView.swift index 1d60a7380..94697a646 100644 --- a/TablePro/ContentView.swift +++ b/TablePro/ContentView.swift @@ -65,10 +65,10 @@ struct ContentView: View { // Always confirm before disconnecting Task { @MainActor in let confirmed = await AlertHelper.confirmDestructive( - title: "Disconnect", - message: "Are you sure you want to disconnect from this database?", - confirmButton: "Disconnect", - cancelButton: "Cancel" + title: String(localized: "Disconnect"), + message: String(localized: "Are you sure you want to disconnect from this database?"), + confirmButton: String(localized: "Disconnect"), + cancelButton: String(localized: "Cancel") ) if confirmed { diff --git a/TablePro/Core/SSH/SSHTunnelManager.swift b/TablePro/Core/SSH/SSHTunnelManager.swift index 9d00110de..472e35ce5 100644 --- a/TablePro/Core/SSH/SSHTunnelManager.swift +++ b/TablePro/Core/SSH/SSHTunnelManager.swift @@ -24,17 +24,17 @@ enum SSHTunnelError: Error, LocalizedError { var errorDescription: String? { switch self { case .tunnelCreationFailed(let message): - return "SSH tunnel creation failed: \(message)" + return String(localized: "SSH tunnel creation failed: \(message)") case .tunnelAlreadyExists(let id): - return "SSH tunnel already exists for connection: \(id)" + return String(localized: "SSH tunnel already exists for connection: \(id.uuidString)") case .noAvailablePort: - return "No available local port for SSH tunnel" + return String(localized: "No available local port for SSH tunnel") case .sshCommandNotFound: - return "SSH command not found. Please ensure OpenSSH is installed." + return String(localized: "SSH command not found. Please ensure OpenSSH is installed.") case .authenticationFailed: - return "SSH authentication failed. Check your credentials or private key." + return String(localized: "SSH authentication failed. Check your credentials or private key.") case .connectionTimeout: - return "SSH connection timed out" + return String(localized: "SSH connection timed out") } } } diff --git a/TablePro/Core/Services/SQLFormatterTypes.swift b/TablePro/Core/Services/SQLFormatterTypes.swift index 578630c75..0dcba1ed7 100644 --- a/TablePro/Core/Services/SQLFormatterTypes.swift +++ b/TablePro/Core/Services/SQLFormatterTypes.swift @@ -50,13 +50,13 @@ enum SQLFormatterError: LocalizedError { var errorDescription: String? { switch self { case .emptyInput: - return "Cannot format empty SQL" + return String(localized: "Cannot format empty SQL") case .dialectUnsupported(let type): - return "Formatting not supported for \(type.rawValue)" + return String(localized: "Formatting not supported for \(type.rawValue)") case .invalidCursorPosition(let pos, let max): - return "Cursor position \(pos) exceeds SQL length (\(max))" + return String(localized: "Cursor position \(pos) exceeds SQL length (\(max))") case .internalError(let message): - return "Formatter error: \(message)" + return String(localized: "Formatter error: \(message)") } } } diff --git a/TablePro/Core/Storage/AppSettingsManager.swift b/TablePro/Core/Storage/AppSettingsManager.swift index eeec8fd4a..bfc12acff 100644 --- a/TablePro/Core/Storage/AppSettingsManager.swift +++ b/TablePro/Core/Storage/AppSettingsManager.swift @@ -18,6 +18,7 @@ final class AppSettingsManager: ObservableObject { @Published var general: GeneralSettings { didSet { + general.language.apply() storage.saveGeneral(general) notifyChange(domain: "general", notification: .generalSettingsDidChange) } @@ -82,6 +83,7 @@ final class AppSettingsManager: ObservableObject { // Apply appearance settings immediately appearance.theme.apply() + general.language.apply() // Load editor theme settings into cache (pass settings directly to avoid circular dependency) SQLEditorTheme.reloadFromSettings(editor) diff --git a/TablePro/Core/Utilities/AlertHelper.swift b/TablePro/Core/Utilities/AlertHelper.swift index fa719dbaf..c580e539a 100644 --- a/TablePro/Core/Utilities/AlertHelper.swift +++ b/TablePro/Core/Utilities/AlertHelper.swift @@ -25,8 +25,8 @@ final class AlertHelper { static func confirmDestructive( title: String, message: String, - confirmButton: String = "OK", - cancelButton: String = "Cancel", + confirmButton: String = String(localized: "OK"), + cancelButton: String = String(localized: "Cancel"), window: NSWindow? = nil ) async -> Bool { let alert = NSAlert() @@ -65,8 +65,8 @@ final class AlertHelper { static func confirmCritical( title: String, message: String, - confirmButton: String = "Execute", - cancelButton: String = "Cancel", + confirmButton: String = String(localized: "Execute"), + cancelButton: String = String(localized: "Cancel"), window: NSWindow? = nil ) async -> Bool { let alert = NSAlert() @@ -160,7 +160,7 @@ final class AlertHelper { alert.messageText = title alert.informativeText = message alert.alertStyle = .critical - alert.addButton(withTitle: "OK") + alert.addButton(withTitle: String(localized: "OK")) if let window = window { alert.beginSheetModal(for: window) { _ in @@ -188,7 +188,7 @@ final class AlertHelper { alert.messageText = title alert.informativeText = message alert.alertStyle = .informational - alert.addButton(withTitle: "OK") + alert.addButton(withTitle: String(localized: "OK")) if let window = window { alert.beginSheetModal(for: window) { _ in diff --git a/TablePro/Core/Validation/SettingsValidation.swift b/TablePro/Core/Validation/SettingsValidation.swift index 428a80e3d..0c7191ed5 100644 --- a/TablePro/Core/Validation/SettingsValidation.swift +++ b/TablePro/Core/Validation/SettingsValidation.swift @@ -20,13 +20,13 @@ enum SettingsValidationError: LocalizedError { var errorDescription: String? { switch self { case .stringTooLong(let field, let maxLength): - return "\(field) must be \(maxLength) characters or less" + return String(localized: "\(field) must be \(maxLength) characters or less") case .stringEmpty(let field): - return "\(field) cannot be empty" + return String(localized: "\(field) cannot be empty") case .intOutOfRange(let field, let min, let max): - return "\(field) must be between \(min.formatted()) and \(max.formatted())" + return String(localized: "\(field) must be between \(min.formatted()) and \(max.formatted())") case .intNegative(let field): - return "\(field) cannot be negative" + return String(localized: "\(field) cannot be negative") } } } diff --git a/TablePro/Models/AppSettings.swift b/TablePro/Models/AppSettings.swift index fbb76851e..1e39eb556 100644 --- a/TablePro/Models/AppSettings.swift +++ b/TablePro/Models/AppSettings.swift @@ -20,8 +20,33 @@ enum StartupBehavior: String, Codable, CaseIterable, Identifiable { var displayName: String { switch self { - case .showWelcome: return "Show Welcome Screen" - case .reopenLast: return "Reopen Last Session" + case .showWelcome: return String(localized: "Show Welcome Screen") + case .reopenLast: return String(localized: "Reopen Last Session") + } + } +} + +/// App language options +enum AppLanguage: String, Codable, CaseIterable, Identifiable { + case system = "system" + case english = "en" + case vietnamese = "vi" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .system: return String(localized: "System") + case .english: return "English" + case .vietnamese: return "Tiếng Việt" + } + } + + func apply() { + if self == .system { + UserDefaults.standard.removeObject(forKey: "AppleLanguages") + } else { + UserDefaults.standard.set([rawValue], forKey: "AppleLanguages") } } } @@ -29,6 +54,7 @@ enum StartupBehavior: String, Codable, CaseIterable, Identifiable { /// General app settings struct GeneralSettings: Codable, Equatable { var startupBehavior: StartupBehavior + var language: AppLanguage var automaticallyCheckForUpdates: Bool /// Query execution timeout in seconds (0 = no limit) @@ -36,16 +62,19 @@ struct GeneralSettings: Codable, Equatable { static let `default` = GeneralSettings( startupBehavior: .showWelcome, + language: .system, automaticallyCheckForUpdates: true, queryTimeoutSeconds: 60 ) init( startupBehavior: StartupBehavior = .showWelcome, + language: AppLanguage = .system, automaticallyCheckForUpdates: Bool = true, queryTimeoutSeconds: Int = 60 ) { self.startupBehavior = startupBehavior + self.language = language self.automaticallyCheckForUpdates = automaticallyCheckForUpdates self.queryTimeoutSeconds = queryTimeoutSeconds } @@ -53,6 +82,7 @@ struct GeneralSettings: Codable, Equatable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) startupBehavior = try container.decode(StartupBehavior.self, forKey: .startupBehavior) + language = try container.decodeIfPresent(AppLanguage.self, forKey: .language) ?? .system automaticallyCheckForUpdates = try container.decodeIfPresent(Bool.self, forKey: .automaticallyCheckForUpdates) ?? true queryTimeoutSeconds = try container.decodeIfPresent(Int.self, forKey: .queryTimeoutSeconds) ?? 60 } @@ -70,9 +100,9 @@ enum AppTheme: String, Codable, CaseIterable, Identifiable { var displayName: String { switch self { - case .system: return "System" - case .light: return "Light" - case .dark: return "Dark" + case .system: return String(localized: "System") + case .light: return String(localized: "Light") + case .dark: return String(localized: "Dark") } } @@ -105,8 +135,15 @@ enum AccentColorOption: String, Codable, CaseIterable, Identifiable { var displayName: String { switch self { - case .system: return "System" - default: return rawValue.capitalized + case .system: return String(localized: "System") + case .blue: return String(localized: "Blue") + case .purple: return String(localized: "Purple") + case .pink: return String(localized: "Pink") + case .red: return String(localized: "Red") + case .orange: return String(localized: "Orange") + case .yellow: return String(localized: "Yellow") + case .green: return String(localized: "Green") + case .graphite: return String(localized: "Graphite") } } @@ -238,10 +275,10 @@ enum DataGridRowHeight: Int, Codable, CaseIterable, Identifiable { var displayName: String { switch self { - case .compact: return "Compact" - case .normal: return "Normal" - case .comfortable: return "Comfortable" - case .spacious: return "Spacious" + case .compact: return String(localized: "Compact") + case .normal: return String(localized: "Normal") + case .comfortable: return String(localized: "Comfortable") + case .spacious: return String(localized: "Spacious") } } } @@ -259,12 +296,12 @@ enum DateFormatOption: String, Codable, CaseIterable, Identifiable { var displayName: String { switch self { - case .iso8601: return "ISO 8601 (2024-12-31 23:59:59)" - case .iso8601Date: return "ISO Date (2024-12-31)" - case .usLong: return "US Long (12/31/2024 11:59:59 PM)" - case .usShort: return "US Short (12/31/2024)" - case .euLong: return "EU Long (31/12/2024 23:59:59)" - case .euShort: return "EU Short (31/12/2024)" + case .iso8601: return String(localized: "ISO 8601 (2024-12-31 23:59:59)") + case .iso8601Date: return String(localized: "ISO Date (2024-12-31)") + case .usLong: return String(localized: "US Long (12/31/2024 11:59:59 PM)") + case .usShort: return String(localized: "US Short (12/31/2024)") + case .euLong: return String(localized: "EU Long (31/12/2024 23:59:59)") + case .euShort: return String(localized: "EU Short (31/12/2024)") } } @@ -314,11 +351,11 @@ struct DataGridSettings: Codable, Equatable { let maxLength = SettingsValidationRules.nullDisplayMaxLength if sanitized.isEmpty { - return "NULL display cannot be empty" + return String(localized: "NULL display cannot be empty") } else if sanitized.count > maxLength { - return "NULL display must be \(maxLength) characters or less" + return String(localized: "NULL display must be \(maxLength) characters or less") } else if nullDisplay != sanitized { - return "NULL display contains invalid characters (newlines/tabs)" + return String(localized: "NULL display contains invalid characters (newlines/tabs)") } return nil } @@ -327,7 +364,7 @@ struct DataGridSettings: Codable, Equatable { var defaultPageSizeValidationError: String? { let range = SettingsValidationRules.defaultPageSizeRange if defaultPageSize < range.lowerBound || defaultPageSize > range.upperBound { - return "Page size must be between \(range.lowerBound.formatted()) and \(range.upperBound.formatted())" + return String(localized: "Page size must be between \(range.lowerBound.formatted()) and \(range.upperBound.formatted())") } return nil } @@ -362,7 +399,7 @@ struct HistorySettings: Codable, Equatable { /// Validation error for maxEntries var maxEntriesValidationError: String? { if maxEntries < 0 { - return "Maximum entries cannot be negative" + return String(localized: "Maximum entries cannot be negative") } return nil } @@ -370,7 +407,7 @@ struct HistorySettings: Codable, Equatable { /// Validation error for maxDays var maxDaysValidationError: String? { if maxDays < 0 { - return "Maximum days cannot be negative" + return String(localized: "Maximum days cannot be negative") } return nil } diff --git a/TablePro/Models/ConnectionToolbarState.swift b/TablePro/Models/ConnectionToolbarState.swift index 9e7c2404e..544594e58 100644 --- a/TablePro/Models/ConnectionToolbarState.swift +++ b/TablePro/Models/ConnectionToolbarState.swift @@ -73,22 +73,22 @@ enum ToolbarConnectionState: Equatable { /// Human-readable description var description: String { switch self { - case .disconnected: return "Disconnected" - case .connecting: return "Connecting..." - case .connected: return "Connected" - case .executing: return "Executing..." - case .error(let message): return "Error: \(message)" + case .disconnected: return String(localized: "Disconnected") + case .connecting: return String(localized: "Connecting...") + case .connected: return String(localized: "Connected") + case .executing: return String(localized: "Executing...") + case .error(let message): return String(localized: "Error: \(message)") } } /// Short label for toolbar display var label: String { switch self { - case .disconnected: return "Disconnected" - case .connecting: return "Connecting" - case .connected: return "Connected" - case .executing: return "Executing" - case .error: return "Error" + case .disconnected: return String(localized: "Disconnected") + case .connecting: return String(localized: "Connecting") + case .connected: return String(localized: "Connected") + case .executing: return String(localized: "Executing") + case .error: return String(localized: "Error") } } @@ -174,15 +174,15 @@ final class ConnectionToolbarState: ObservableObject { var parts: [String] = [connectionState.description] if let latency = latencyMs { - parts.append("Latency: \(latency)ms") + parts.append(String(localized: "Latency: \(latency)ms")) } if let lag = replicationLagSeconds { - parts.append("Replication lag: \(lag)s") + parts.append(String(localized: "Replication lag: \(lag)s")) } if isReadOnly { - parts.append("Read-only") + parts.append(String(localized: "Read-only")) } return parts.joined(separator: " • ") diff --git a/TablePro/Models/DatabaseConnection.swift b/TablePro/Models/DatabaseConnection.swift index 9179a1f45..69cf86587 100644 --- a/TablePro/Models/DatabaseConnection.swift +++ b/TablePro/Models/DatabaseConnection.swift @@ -17,6 +17,13 @@ enum SSHAuthMethod: String, CaseIterable, Identifiable, Codable { var id: String { rawValue } + var displayName: String { + switch self { + case .password: return String(localized: "Password") + case .privateKey: return String(localized: "Private Key") + } + } + var iconName: String { switch self { case .password: return "key.fill" @@ -63,11 +70,11 @@ enum SSLMode: String, CaseIterable, Identifiable, Codable { var description: String { switch self { - case .disabled: return "No SSL encryption" - case .preferred: return "Use SSL if available" - case .required: return "Require SSL, skip verification" - case .verifyCa: return "Verify server certificate" - case .verifyIdentity: return "Verify certificate and hostname" + case .disabled: return String(localized: "No SSL encryption") + case .preferred: return String(localized: "Use SSL if available") + case .required: return String(localized: "Require SSL, skip verification") + case .verifyCa: return String(localized: "Verify server certificate") + case .verifyIdentity: return String(localized: "Verify certificate and hostname") } } } @@ -157,6 +164,20 @@ enum ConnectionColor: String, CaseIterable, Identifiable, Codable { var id: String { rawValue } + var displayName: String { + switch self { + case .none: return String(localized: "None") + case .red: return String(localized: "Red") + case .orange: return String(localized: "Orange") + case .yellow: return String(localized: "Yellow") + case .green: return String(localized: "Green") + case .blue: return String(localized: "Blue") + case .purple: return String(localized: "Purple") + case .pink: return String(localized: "Pink") + case .gray: return String(localized: "Gray") + } + } + /// SwiftUI Color for display var color: Color { switch self { diff --git a/TablePro/Models/ExportModels.swift b/TablePro/Models/ExportModels.swift index e8f67f00f..e2cd045e1 100644 --- a/TablePro/Models/ExportModels.swift +++ b/TablePro/Models/ExportModels.swift @@ -63,6 +63,14 @@ enum CSVQuoteHandling: String, CaseIterable, Identifiable { case never = "Never" var id: String { rawValue } + + var displayName: String { + switch self { + case .always: return String(localized: "Always") + case .asNeeded: return String(localized: "Quote if needed") + case .never: return String(localized: "Never") + } + } } /// Line break format for CSV export diff --git a/TablePro/Models/ImportModels.swift b/TablePro/Models/ImportModels.swift index d585cf4cb..945e5fd24 100644 --- a/TablePro/Models/ImportModels.swift +++ b/TablePro/Models/ImportModels.swift @@ -58,23 +58,23 @@ enum ImportError: LocalizedError { var errorDescription: String? { switch self { case .fileNotFound: - return "File not found" + return String(localized: "File not found") case .fileReadFailed(let message): - return "Failed to read file: \(message)" + return String(localized: "Failed to read file: \(message)") case .decompressFailed: - return "Failed to decompress .gz file" + return String(localized: "Failed to decompress .gz file") case .parseStatementFailed(let line, let reason): - return "Failed to parse statement at line \(line): \(reason)" + return String(localized: "Failed to parse statement at line \(line): \(reason)") case .importFailed(_, let line, let error): - return "Import failed at line \(line): \(error)" + return String(localized: "Import failed at line \(line): \(error)") case .cancelled: - return "Import cancelled by user" + return String(localized: "Import cancelled by user") case .invalidEncoding: - return "Invalid file encoding. Try a different encoding option." + return String(localized: "Invalid file encoding. Try a different encoding option.") case .rollbackFailed(let message): - return "CRITICAL: Transaction rollback failed - database may be in inconsistent state: \(message)" + return String(localized: "CRITICAL: Transaction rollback failed - database may be in inconsistent state: \(message)") case .foreignKeyCleanupFailed(let message): - return "WARNING: Failed to re-enable foreign key checks: \(message). Please manually verify FK constraints are enabled." + return String(localized: "WARNING: Failed to re-enable foreign key checks: \(message). Please manually verify FK constraints are enabled.") } } } diff --git a/TablePro/Models/License.swift b/TablePro/Models/License.swift index 46b78ee47..2f45ce1c5 100644 --- a/TablePro/Models/License.swift +++ b/TablePro/Models/License.swift @@ -20,12 +20,12 @@ enum LicenseStatus: String, Codable { var displayName: String { switch self { - case .unlicensed: return "Unlicensed" - case .active: return "Active" - case .expired: return "Expired" - case .suspended: return "Suspended" - case .deactivated: return "Deactivated" - case .validationFailed: return "Validation Failed" + case .unlicensed: return String(localized: "Unlicensed") + case .active: return String(localized: "Active") + case .expired: return String(localized: "Expired") + case .suspended: return String(localized: "Suspended") + case .deactivated: return String(localized: "Deactivated") + case .validationFailed: return String(localized: "Validation Failed") } } diff --git a/TablePro/Models/ParsedRow.swift b/TablePro/Models/ParsedRow.swift index 055c54051..a14f9e640 100644 --- a/TablePro/Models/ParsedRow.swift +++ b/TablePro/Models/ParsedRow.swift @@ -36,13 +36,13 @@ enum RowParseError: LocalizedError { var errorDescription: String? { switch self { case .emptyClipboard: - return "Clipboard is empty or contains no text data." + return String(localized: "Clipboard is empty or contains no text data.") case .noValidRows: - return "No valid rows found in clipboard data." + return String(localized: "No valid rows found in clipboard data.") case .columnCountMismatch(let expected, let actual, let line): - return "Column count mismatch on line \(line): expected \(expected) columns, found \(actual)." + return String(localized: "Column count mismatch on line \(line): expected \(expected) columns, found \(actual).") case .invalidFormat(let reason): - return "Invalid data format: \(reason)" + return String(localized: "Invalid data format: \(reason)") } } } diff --git a/TablePro/Models/QueryResult.swift b/TablePro/Models/QueryResult.swift index 4fdc6ac7d..34715a34e 100644 --- a/TablePro/Models/QueryResult.swift +++ b/TablePro/Models/QueryResult.swift @@ -71,13 +71,13 @@ enum DatabaseError: Error, LocalizedError { case .queryFailed(let message): return message case .invalidCredentials: - return "Invalid username or password" + return String(localized: "Invalid username or password") case .fileNotFound(let path): - return "Database file not found: \(path)" + return String(localized: "Database file not found: \(path)") case .notConnected: - return "Not connected to database" + return String(localized: "Not connected to database") case .unsupportedOperation: - return "This operation is not supported" + return String(localized: "This operation is not supported") } } } diff --git a/TablePro/Models/TableFilter.swift b/TablePro/Models/TableFilter.swift index 6452b8302..d03199e7c 100644 --- a/TablePro/Models/TableFilter.swift +++ b/TablePro/Models/TableFilter.swift @@ -48,24 +48,24 @@ enum FilterOperator: String, CaseIterable, Identifiable, Codable { /// Display name for UI var displayName: String { switch self { - case .equal: return "equals" - case .notEqual: return "not equals" - case .contains: return "contains" - case .notContains: return "not contains" - case .startsWith: return "starts with" - case .endsWith: return "ends with" - case .greaterThan: return "greater than" - case .greaterOrEqual: return "greater or equal" - case .lessThan: return "less than" - case .lessOrEqual: return "less or equal" - case .isNull: return "is NULL" - case .isNotNull: return "is not NULL" - case .isEmpty: return "is empty" - case .isNotEmpty: return "is not empty" - case .inList: return "in list" - case .notInList: return "not in list" - case .between: return "between" - case .regex: return "matches regex" + case .equal: return String(localized: "equals") + case .notEqual: return String(localized: "not equals") + case .contains: return String(localized: "contains") + case .notContains: return String(localized: "not contains") + case .startsWith: return String(localized: "starts with") + case .endsWith: return String(localized: "ends with") + case .greaterThan: return String(localized: "greater than") + case .greaterOrEqual: return String(localized: "greater or equal") + case .lessThan: return String(localized: "less than") + case .lessOrEqual: return String(localized: "less or equal") + case .isNull: return String(localized: "is NULL") + case .isNotNull: return String(localized: "is not NULL") + case .isEmpty: return String(localized: "is empty") + case .isNotEmpty: return String(localized: "is not empty") + case .inList: return String(localized: "in list") + case .notInList: return String(localized: "not in list") + case .between: return String(localized: "between") + case .regex: return String(localized: "matches regex") } } } @@ -128,21 +128,21 @@ struct TableFilter: Identifiable, Equatable, Codable { var validationError: String? { if columnName == Self.rawSQLColumn { if rawSQL?.isEmpty ?? true { - return "Raw SQL cannot be empty" + return String(localized: "Raw SQL cannot be empty") } return nil } if columnName.isEmpty { - return "Please select a column" + return String(localized: "Please select a column") } if filterOperator.requiresValue { if value.isEmpty { - return "Value is required" + return String(localized: "Value is required") } if filterOperator.requiresSecondValue && (secondValue?.isEmpty ?? true) { - return "Second value is required for BETWEEN" + return String(localized: "Second value is required for BETWEEN") } } diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings new file mode 100644 index 000000000..5a171e9e1 --- /dev/null +++ b/TablePro/Resources/Localizable.xcstrings @@ -0,0 +1,6478 @@ +{ + "sourceLanguage": "en", + "strings": { + "": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "" + } + } + } + }, + "--": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "--" + } + } + } + }, + "—": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "—" + } + } + } + }, + ".%@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": ".%@" + } + } + } + }, + "''": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "''" + } + } + } + }, + "(%@)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "(%@)" + } + } + } + }, + "(%lld active)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "(%lld đang hoạt động)" + } + } + } + }, + "(%lld)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "(%lld)" + } + } + } + }, + "(optional)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "(tùy chọn)" + } + } + } + }, + "/path/to/ca-cert.pem": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "/đường/dẫn/tới/ca-cert.pem" + } + } + } + }, + "/path/to/database.sqlite": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "/đường/dẫn/tới/database.sqlite" + } + } + } + }, + "%@ (%lld/%lld)": { + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%1$@ (%2$lld/%3$lld)" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "%1$@ (%2$lld/%3$lld)" + } + } + } + }, + "%@ cannot be empty": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@ không được để trống" + } + } + } + }, + "%@ cannot be negative": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@ không được là số âm" + } + } + } + }, + "%@ ms": { + "localizations": { + "vi": { + "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" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "%1$@ phải có %2$lld ký tự trở xuống" + } + } + } + }, + "%@ 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$@" + } + } + } + }, + "%@ rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@ dòng" + } + } + } + }, + "%@ s": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@ giây" + } + } + } + }, + "%@ seconds": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@ giây" + } + } + } + }, + "%@/%@ rows": { + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%1$@/%2$@ rows" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "%1$@/%2$@ dòng" + } + } + } + }, + "%@ms": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@ms" + } + } + } + }, + "%@s": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%@s" + } + } + } + }, + "%lld": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lld" + } + } + } + }, + "%lld of %lld": { + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%1$lld of %2$lld" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "%1$lld / %2$lld" + } + } + } + }, + "%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" + } + } + } + }, + "%lld pt": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lld pt" + } + } + } + }, + "%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" + } + } + } + }, + "%lld seconds": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lld giây" + } + } + } + }, + "%lld skipped (no options)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lld bị bỏ qua (không có tùy chọn)" + } + } + } + }, + "%lld statements": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lld câu lệnh" + } + } + } + }, + "%lld statements executed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đã thực thi %lld câu lệnh" + } + } + } + }, + "%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" + } + } + } + }, + "%lld tables": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lld bảng" + } + } + } + }, + "%lld-%lld of %@ rows": { + "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" + } + } + } + }, + "%lldm %llds": { + "localizations": { + "en": { + "stringUnit": { + "state": "new", + "value": "%1$lldm %2$llds" + } + }, + "vi": { + "stringUnit": { + "state": "translated", + "value": "%lldm %llds" + } + } + } + }, + "•": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "•" + } + } + } + }, + "••••••••": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "••••••••" + } + } + } + }, + "<1ms": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "<1ms" + } + } + } + }, + "=": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "=" + } + } + } + }, + "~/.ssh/id_rsa": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "~/.ssh/id_rsa" + } + } + } + }, + "⌘K": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + } + } + }, + "⌘T": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "⌘T" + } + } + } + }, + "0": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "0" + } + } + } + }, + "1 (no batching)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "1 (không gom nhóm)" + } + } + } + }, + "1 year": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "1 năm" + } + } + } + }, + "1,000": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "1,000" + } + } + } + }, + "1,000 rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "1.000 dòng" + } + } + } + }, + "2 spaces": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "2 dấu cách" + } + } + } + }, + "4 spaces": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "4 dấu cách" + } + } + } + }, + "5,000": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "5,000" + } + } + } + }, + "5,000 rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "5.000 dòng" + } + } + } + }, + "7 days": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "7 ngày" + } + } + } + }, + "8 spaces": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "8 dấu cách" + } + } + } + }, + "10,000": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "10,000" + } + } + } + }, + "10,000 rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "10.000 dòng" + } + } + } + }, + "22": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "22" + } + } + } + }, + "30 days": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "30 ngày" + } + } + } + }, + "90 days": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "90 ngày" + } + } + } + }, + "100": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "100" + } + } + } + }, + "100 rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "100 dòng" + } + } + } + }, + "500": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "500" + } + } + } + }, + "500 rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "500 dòng" + } + } + } + }, + "Accent Color:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Màu nhấn:" + } + } + } + }, + "Activate": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kích hoạt" + } + } + } + }, + "Activation Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kích hoạt thất bại" + } + } + } + }, + "Active": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang hoạt động" + } + } + } + }, + "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" + } + } + } + }, + "Add Check Constraint": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm ràng buộc kiểm tra" + } + } + } + }, + "Add Column": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm cột" + } + } + } + }, + "Add columns first": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm cột trước" + } + } + } + }, + "Add Filter (Cmd+Shift+F)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm bộ lọc (Cmd+Shift+F)" + } + } + } + }, + "Add Foreign Key": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm khóa ngoại" + } + } + } + }, + "Add Index": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm chỉ mục" + } + } + } + }, + "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 Row": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thêm dòng" + } + } + } + }, + "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" + } + } + } + }, + "All %lld rows selected": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đã chọn tất cả %lld dòng" + } + } + } + }, + "ALL DATABASES": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "TẤT CẢ CƠ SỞ DỮ LIỆU" + } + } + } + }, + "Always": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Luôn luôn" + } + } + } + }, + "and": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "và" + } + } + } + }, + "AND": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "AND" + } + } + } + }, + "Appearance": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giao diện" + } + } + } + }, + "Appearance:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giao diện:" + } + } + } + }, + "Apply All": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Áp dụng tất cả" + } + } + } + }, + "Apply Changes": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Áp dụng thay đổi" + } + } + } + }, + "Apply This Filter": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Áp dụng bộ lọc này" + } + } + } + }, + "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 disconnect from this database?": { + "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?" + } + } + } + }, + "Authentication": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xác thực" + } + } + } + }, + "AUTO": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "TỰ ĐỘNG" + } + } + } + }, + "Auto cleanup on startup": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tự động dọn dẹp khi khởi động" + } + } + } + }, + "Auto Increment": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tự động tăng" + } + } + } + }, + "Auto-indent": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tự động thụt lề" + } + } + } + }, + "Automatically check for updates": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tự động kiểm tra cập nhật" + } + } + } + }, + "Avg Row": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "TB dòng" + } + } + } + }, + "between": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "giữa" + } + } + } + }, + "Blue": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xanh dương" + } + } + } + }, + "Browse": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Duyệt" + } + } + } + }, + "Browse...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Duyệt..." + } + } + } + }, + "Cancel": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy" + } + } + } + }, + "Cannot format empty SQL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không thể định dạng SQL trống" + } + } + } + }, + "Cascade": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cascade" + } + } + } + }, + "Change File": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đổi tệp" + } + } + } + }, + "Change File...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đổi tệp..." + } + } + } + }, + "Character Set": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bộ ký tự" + } + } + } + }, + "Charset (e.g., utf8mb4)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bộ ký tự (vd: utf8mb4)" + } + } + } + }, + "Check for Updates...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kiểm tra cập nhật..." + } + } + } + }, + "Clear": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa" + } + } + } + }, + "Clear All": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa tất cả" + } + } + } + }, + "Clear All History?": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa toàn bộ lịch sử?" + } + } + } + }, + "Clear all query history": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa toàn bộ lịch sử truy vấn" + } + } + } + }, + "Clear History...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa lịch sử..." + } + } + } + }, + "Clear Query (⌘+Delete)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa truy vấn (⌘+Delete)" + } + } + } + }, + "Clear Search": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa tìm kiếm" + } + } + } + }, + "Clear Selection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bỏ chọn" + } + } + } + }, + "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 create your first connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhấn + để tạo kết nối đầu tiên" + } + } + } + }, + "Click a table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhấn vào một bảng" + } + } + } + }, + "Click to show all tables with metadata": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhấn để hiện tất cả bảng với siêu dữ liệu" + } + } + } + }, + "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." + } + } + } + }, + "Close": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đóng" + } + } + } + }, + "Close (ESC)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đóng (ESC)" + } + } + } + }, + "Close Other Tabs": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đóng các tab khác" + } + } + } + }, + "Close Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đóng tab" + } + } + } + }, + "Closing this tab will discard all unsaved changes.": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đóng tab này sẽ hủy tất cả thay đổi chưa lưu." + } + } + } + }, + "Collation": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đối chiếu" + } + } + } + }, + "Color": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Màu sắc" + } + } + } + }, + "Column": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cột" + } + } + } + }, + "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." + } + } + } + }, + "Column Details": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chi tiết cột" + } + } + } + }, + "Column name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên cột" + } + } + } + }, + "Column Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên cột" + } + } + } + }, + "Columns (comma-separated)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Các cột (phân tách bằng dấu phẩy)" + } + } + } + }, + "Comfortable": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thoải mái" + } + } + } + }, + "Comment": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Ghi chú" + } + } + } + }, + "Compact": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thu gọn" + } + } + } + }, + "Compress the file using Gzip": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nén tệp bằng Gzip" + } + } + } + }, + "Connect": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối" + } + } + } + }, + "Connected": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đã kết nối" + } + } + } + }, + "Connecting": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang kết nối" + } + } + } + }, + "Connecting...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang kết nối..." + } + } + } + }, + "Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối" + } + } + } + }, + "Connection Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối thất bại" + } + } + } + }, + "Connection name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên kết nối" + } + } + } + }, + "Connection Status": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trạng thái kết nối" + } + } + } + }, + "Connection Switcher": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển đổi kết nối" + } + } + } + }, + "Connection test failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kiểm tra kết nối thất bại" + } + } + } + }, + "Constraint name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên ràng buộc" + } + } + } + }, + "contains": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "chứa" + } + } + } + }, + "Convert line break to space": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển xuống dòng thành dấu cách" + } + } + } + }, + "Convert NULL to empty": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển NULL thành rỗng" + } + } + } + }, + "Convert NULL to EMPTY": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển NULL thành RỖNG" + } + } + } + }, + "Copied!": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đã sao chép!" + } + } + } + }, + "Copy": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sao chép" + } + } + } + }, + "Copy Cell Value": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sao chép giá trị ô" + } + } + } + }, + "Copy Column Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sao chép tên cột" + } + } + } + }, + "Copy Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sao chép tên" + } + } + } + }, + "Copy SQL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sao chép 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" + } + } + } + }, + "Counting...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang đếm..." + } + } + } + }, + "Create": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo" + } + } + } + }, + "Create connection...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo kết nối..." + } + } + } + }, + "Create Database": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo cơ sở dữ liệu" + } + } + } + }, + "Create new database": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo cơ sở dữ liệu mới" + } + } + } + }, + "Create New Table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo bảng mới" + } + } + } + }, + "Create New Table...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo bảng mới..." + } + } + } + }, + "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..." + } + } + } + }, + "Create New View...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo view mới..." + } + } + } + }, + "Create Table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo bảng" + } + } + } + }, + "Created": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đã tạo" + } + } + } + }, + "Creating...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang tạo..." + } + } + } + }, + "CRITICAL: Transaction rollback failed - database may be in inconsistent state: %@": { + "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: %@" + } + } + } + }, + "CURDATE()": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "CURDATE()" + } + } + } + }, + "current": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "hiện tại" + } + } + } + }, + "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: %@ (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_TIMESTAMP()": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "CURRENT_TIMESTAMP()" + } + } + } + }, + "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)" + } + } + } + }, + "CURTIME()": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "CURTIME()" + } + } + } + }, + "Cut": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cắt" + } + } + } + }, + "Dark": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tối" + } + } + } + }, + "Data": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dữ liệu" + } + } + } + }, + "Data Grid": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưới dữ liệu" + } + } + } + }, + "Data Size": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kích thước dữ liệu" + } + } + } + }, + "Data Type:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kiểu dữ liệu:" + } + } + } + }, + "Database": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "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: %@" + } + } + } + }, + "Database Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên cơ sở dữ liệu" + } + } + } + }, + "Database Switcher": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển đổi cơ sở dữ liệu" + } + } + } + }, + "database_name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "database_name" + } + } + } + }, + "Database: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cơ sở dữ liệu: %@" + } + } + } + }, + "Database/Schema:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cơ sở dữ liệu/Schema:" + } + } + } + }, + "Date format:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Định dạng ngày:" + } + } + } + }, + "Deactivate": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy kích hoạt" + } + } + } + }, + "Deactivate License?": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy kích hoạt giấy phép?" + } + } + } + }, + "Deactivate...": { + "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" + } + } + } + }, + "Deactivation Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy kích hoạt thất bại" + } + } + } + }, + "Decimal": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thập phân" + } + } + } + }, + "Default": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mặc định" + } + } + } + }, + "Default Column": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cột mặc định" + } + } + } + }, + "Default Operator": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Toán tử mặc định" + } + } + } + }, + "Default page size:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kích thước trang mặc định:" + } + } + } + }, + "Default value": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giá trị mặc định" + } + } + } + }, + "Default Value": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giá trị mặc định" + } + } + } + }, + "Default:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mặc định:" + } + } + } + }, + "Delete": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa" + } + } + } + }, + "Delete \"%@\"": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa \"%@\"" + } + } + } + }, + "Delete (⌫)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa (⌫)" + } + } + } + }, + "Delete Check Constraint": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa ràng buộc kiểm tra" + } + } + } + }, + "Delete Column": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa cột" + } + } + } + }, + "Delete Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa kết nối" + } + } + } + }, + "Delete Foreign Key": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa khóa ngoại" + } + } + } + }, + "Delete Index": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa chỉ mục" + } + } + } + }, + "Delete Preset": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa mẫu đặt trước" + } + } + } + }, + "Delimiter": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dấu phân cách" + } + } + } + }, + "Disable foreign key checks": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tắt kiểm tra khóa ngoại" + } + } + } + }, + "Discard": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy bỏ" + } + } + } + }, + "Discard Changes?": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy bỏ thay đổi?" + } + } + } + }, + "Discard Unsaved Changes?": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hủy bỏ thay đổi chưa lưu?" + } + } + } + }, + "Disconnect": { + "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" + } + } + } + }, + "Display": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiển thị" + } + } + } + }, + "Don't show this again": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không hiện lại" + } + } + } + }, + "Drop": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa" + } + } + } + }, + "Drop View": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa view" + } + } + } + }, + "Duplicate": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhân bản" + } + } + } + }, + "Duplicate Existing Table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhân bản bảng hiện có" + } + } + } + }, + "Duplicate Filter": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhân bản bộ lọc" + } + } + } + }, + "Duplicate Row": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhân bản dòng" + } + } + } + }, + "Duplicate Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhân bản tab" + } + } + } + }, + "Duplicate Table Structure": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhân bản cấu trúc bảng" + } + } + } + }, + "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." + } + } + } + }, + "Edit": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sửa" + } + } + } + }, + "Edit Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sửa kết nối" + } + } + } + }, + "Edit Details (Double-click)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sửa chi tiết (Nhấp đúp)" + } + } + } + }, + "Edit Row": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sửa dòng" + } + } + } + }, + "Edit View Definition": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sửa định nghĩa view" + } + } + } + }, + "Editing": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang sửa" + } + } + } + }, + "Editor": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trình soạn thảo" + } + } + } + }, + "Email:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Email:" + } + } + } + }, + "Empty": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trống" + } + } + } + }, + "Encoding:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mã hóa:" + } + } + } + }, + "ends with": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "kết thúc bằng" + } + } + } + }, + "Engine": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Engine" + } + } + } + }, + "Engine (e.g., InnoDB)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Engine (vd: 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 database name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập tên cơ sở dữ liệu" + } + } + } + }, + "equals": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "bằng" + } + } + } + }, + "Error": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi" + } + } + } + }, + "Error Applying Changes": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi áp dụng thay đổi" + } + } + } + }, + "Error:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi:" + } + } + } + }, + "Error: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi: %@" + } + } + } + }, + "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" + } + } + } + }, + "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 Short (31/12/2024)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Châu Âu ngắn (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" + } + } + } + }, + "Execute": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thực thi" + } + } + } + }, + "Execute all statements in a single transaction. If any statement fails, all changes are rolled back.": { + "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." + } + } + } + }, + "Executing": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang thực thi" + } + } + } + }, + "Executing...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang thực thi..." + } + } + } + }, + "Expired": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hết hạn" + } + } + } + }, + "Explain": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giải thích" + } + } + } + }, + "Explain Query": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giải thích truy vấn" + } + } + } + }, + "export": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "xuất" + } + } + } + }, + "Export": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất" + } + } + } + }, + "Export completed successfully": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất dữ liệu thành công" + } + } + } + }, + "Export Data": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất dữ liệu" + } + } + } + }, + "Export Data (⌘⇧E)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất dữ liệu (⌘⇧E)" + } + } + } + }, + "Export Error": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi xuất dữ liệu" + } + } + } + }, + "Export multiple tables": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất nhiều bảng" + } + } + } + }, + "Export table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất bảng" + } + } + } + }, + "Export...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuất..." + } + } + } + }, + "Expression (e.g., age >= 0)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Biểu thức (vd: age >= 0)" + } + } + } + }, + "Failed at line %lld": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thất bại tại dòng %lld" + } + } + } + }, + "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 file: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giải nén tệp thất bại: %@" + } + } + } + }, + "Failed to delete template: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa mẫu thất bại: %@" + } + } + } + }, + "Failed to fetch table structure: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lấy cấu trúc bảng thất bại: %@" + } + } + } + }, + "Failed to import DDL: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập DDL 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" + } + } + } + }, + "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 preview using encoding: %@. Try selecting a different text encoding from the encoding picker and reload the preview.": { + "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: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tải bản xem trước thất bại: %@" + } + } + } + }, + "Failed to load tables: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tải danh sách bảng thất bại: %@" + } + } + } + }, + "Failed to load template: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tải mẫu thất bại: %@" + } + } + } + }, + "Failed to parse any columns from table '%@'. Check console for debug info.": { + "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 statement at line %lld: %@": { + "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$@" + } + } + } + }, + "Failed to read file: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đọc tệp thất bại: %@" + } + } + } + }, + "Failed to Save Changes": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu thay đổi thất bại" + } + } + } + }, + "Failed to save template: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu mẫu thất bại: %@" + } + } + } + }, + "FALSE": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "FALSE" + } + } + } + }, + "FIELDS (%lld)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "CÁC TRƯỜNG (%lld)" + } + } + } + }, + "File name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên tệp" + } + } + } + }, + "File not found": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không tìm thấy tệp" + } + } + } + }, + "File Path": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đường dẫn tệp" + } + } + } + }, + "Filter": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bộ lọc" + } + } + } + }, + "Filter Settings": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cài đặt bộ lọc" + } + } + } + }, + "Filter with column": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lọc theo cột" + } + } + } + }, + "Filters": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bộ lọc" + } + } + } + }, + "Font": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Phông chữ" + } + } + } + }, + "Font:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Phông chữ:" + } + } + } + }, + "Forever": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mãi mãi" + } + } + } + }, + "Format Query (⌥⌘F)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Định dạng truy vấn (⌥⌘F)" + } + } + } + }, + "Formatter error: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi định dạng: %@" + } + } + } + }, + "Formatting not supported for %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không hỗ trợ định dạng cho %@" + } + } + } + }, + "General": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tổng quát" + } + } + } + }, + "Generated WHERE Clause": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mệnh đề WHERE đã tạo" + } + } + } + }, + "Go": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đi" + } + } + } + }, + "Graphite": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Than chì" + } + } + } + }, + "Gray": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xám" + } + } + } + }, + "greater or equal": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "lớn hơn hoặc bằng" + } + } + } + }, + "greater than": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "lớn hơn" + } + } + } + }, + "Green": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xanh lá" + } + } + } + }, + "Higher values create fewer INSERT statements, resulting in smaller files and faster imports": { + "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" + } + } + } + }, + "Highlight current line": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đánh dấu dòng hiện tại" + } + } + } + }, + "History": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lịch sử" + } + } + } + }, + "Ignore foreign key checks": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bỏ qua kiểm tra khóa ngoại" + } + } + } + }, + "Import": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập" + } + } + } + }, + "Import cancelled by user": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Người dùng đã hủy nhập" + } + } + } + }, + "Import Data": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập dữ liệu" + } + } + } + }, + "Import Data (⌘⇧I)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập dữ liệu (⌘⇧I)" + } + } + } + }, + "Import Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập thất bại" + } + } + } + }, + "Import failed at line %lld: %@": { + "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$@" + } + } + } + }, + "Import from DDL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập từ DDL" + } + } + } + }, + "Import SQL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập SQL" + } + } + } + }, + "Import Successful": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập thành công" + } + } + } + }, + "Import...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Nhập..." + } + } + } + }, + "in list": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "trong danh sách" + } + } + } + }, + "Include column headers": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bao gồm tiêu đề cột" + } + } + } + }, + "Include NULL values": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bao gồm giá trị NULL" + } + } + } + }, + "INDEX": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "CHỈ MỤC" + } + } + } + }, + "Index name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên chỉ mục" + } + } + } + }, + "Index Size": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kích thước chỉ mục" + } + } + } + }, + "Inspector": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thanh kiểm tra" + } + } + } + }, + "Invalid data format: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Định dạng dữ liệu không hợp lệ: %@" + } + } + } + }, + "Invalid file encoding. Try a different encoding option.": { + "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 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ệ" + } + } + } + }, + "is empty": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "trống" + } + } + } + }, + "is not empty": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "không trống" + } + } + } + }, + "is not NULL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "không phải NULL" + } + } + } + }, + "is NULL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "là NULL" + } + } + } + }, + "ISO 8601 (2024-12-31 23:59:59)": { + "localizations": { + "vi": { + "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)" + } + } + } + }, + "Items": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mục" + } + } + } + }, + "Keep entries for:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giữ mục trong:" + } + } + } + }, + "Keep leading zeros in ZIP codes, phone numbers, and IDs by outputting all values as strings": { + "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" + } + } + } + }, + "Language:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Ngôn ngữ:" + } + } + } + }, + "Last query execution time": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thời gian thực thi truy vấn gần nhất" + } + } + } + }, + "Latency: %lldms": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ trễ: %lldms" + } + } + } + }, + "Length": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ dài" + } + } + } + }, + "Length:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ dài:" + } + } + } + }, + "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" + } + } + } + }, + "License": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giấy phép" + } + } + } + }, + "License Key:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mã giấy phép:" + } + } + } + }, + "Light": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sáng" + } + } + } + }, + "Limit": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giới hạn" + } + } + } + }, + "Line break": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xuống dòng" + } + } + } + }, + "Load": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tải" + } + } + } + }, + "Load Table Template": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tải mẫu bảng" + } + } + } + }, + "Load Template": { + "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..." + } + } + } + }, + "Loading tables...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang tải danh sách bảng..." + } + } + } + }, + "localhost": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "localhost" + } + } + } + }, + "Maintenance": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bảo trì" + } + } + } + }, + "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ẻ" + } + } + } + }, + "Manual": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thủ công" + } + } + } + }, + "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" + } + } + } + }, + "Max %lld characters": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tối đa %lld ký tự" + } + } + } + }, + "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" + } + } + } + }, + "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." + } + } + } + }, + "METADATA": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SIÊU DỮ LIỆU" + } + } + } + }, + "Move Down": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Di chuyển xuống" + } + } + } + }, + "Move Down (⌘↓)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Di chuyển xuống (⌘↓)" + } + } + } + }, + "Move Up": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Di chuyển lên" + } + } + } + }, + "Move Up (⌘↑)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Di chuyển lên (⌘↑)" + } + } + } + }, + "Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên" + } + } + } + }, + "Never": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không bao giờ" + } + } + } + }, + "New Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối mới" + } + } + } + }, + "New Connection (⌘N)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối mới (⌘N)" + } + } + } + }, + "New Connection...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối mới..." + } + } + } + }, + "New Query Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tab truy vấn mới" + } + } + } + }, + "New Query Tab (⌘T)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tab truy vấn mới (⌘T)" + } + } + } + }, + "New Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tab mới" + } + } + } + }, + "New Table...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bảng mới..." + } + } + } + }, + "New View...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "View mới..." + } + } + } + }, + "Next Page (⌘])": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trang sau (⌘])" + } + } + } + }, + "Next Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tab tiếp" + } + } + } + }, + "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 changes to preview": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có thay đổi để xem trước" + } + } + } + }, + "No Check Constraints": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có ràng buộc kiểm tra" + } + } + } + }, + "No Columns Defined": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa có cột nào" + } + } + } + }, + "No connections yet": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa có kết nối nào" + } + } + } + }, + "No database connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa kết nối 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" + } + } + } + }, + "No databases match \"%@\"": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có cơ sở dữ liệu nào khớp \"%@\"" + } + } + } + }, + "No Foreign Keys Yet": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa có khóa ngoại" + } + } + } + }, + "No Indexes Defined": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa có chỉ mục" + } + } + } + }, + "No limit": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không giới hạn" + } + } + } + }, + "No matching connections": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có kết nối 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" + } + } + } + }, + "No matching tables": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có bảng nào khớp" + } + } + } + }, + "No primary key selected (not recommended)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa chọn khóa chính (không khuyến nghị)" + } + } + } + }, + "No rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có dòng" + } + } + } + }, + "No saved templates": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có mẫu đã lưu" + } + } + } + }, + "No Selection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa chọn" + } + } + } + }, + "No SSL encryption": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không mã hóa SSL" + } + } + } + }, + "No Tables": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có bảng" + } + } + } + }, + "No tabs open": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không có tab nào 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." + } + } + } + }, + "None": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không" + } + } + } + }, + "Normal": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bình thường" + } + } + } + }, + "Not connected to database": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa kết nối cơ sở dữ liệu" + } + } + } + }, + "not contains": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "không chứa" + } + } + } + }, + "not equals": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "không bằng" + } + } + } + }, + "not in list": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "không trong danh sách" + } + } + } + }, + "NOT NULL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "NOT NULL" + } + } + } + }, + "NOW()": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "NOW()" + } + } + } + }, + "NULL": { + "localizations": { + "vi": { + "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 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 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:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiển thị NULL:" + } + } + } + }, + "Offset": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Vị trí bắt đầu" + } + } + } + }, + "OK": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + } + } + }, + "Open": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở" + } + } + } + }, + "Open Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở kết nối" + } + } + } + }, + "Open containing folder": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở thư mục chứa" + } + } + } + }, + "Open Database": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở cơ sở dữ liệu" + } + } + } + }, + "Open Database (⌘K)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở cơ sở dữ liệu (⌘K)" + } + } + } + }, + "Open Database...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở cơ sở dữ liệu..." + } + } + } + }, + "Open SQL Editor": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở trình soạn SQL" + } + } + } + }, + "Optional description": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mô tả tùy chọn" + } + } + } + }, + "Options": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tùy chọn" + } + } + } + }, + "OR": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "OR" + } + } + } + }, + "Orange": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cam" + } + } + } + }, + "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$@" + } + } + } + }, + "Pagination": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Phân trang" + } + } + } + }, + "Pagination Settings": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cài đặt phân trang" + } + } + } + }, + "Panel State": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trạng thái bảng điều khiển" + } + } + } + }, + "Password": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mật khẩu" + } + } + } + }, + "Paste": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dán" + } + } + } + }, + "Paste your CREATE TABLE statement below:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dán câu lệnh CREATE TABLE bên dưới:" + } + } + } + }, + "Pin Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Ghim tab" + } + } + } + }, + "Pink": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hồng" + } + } + } + }, + "Please select a column": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Vui lòng chọn một cột" + } + } + } + }, + "Potentially Dangerous Query": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Truy vấn có thể nguy hiểm" + } + } + } + }, + "Precision": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ chính xác" + } + } + } + }, + "Precision:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ chính xác:" + } + } + } + }, + "Preserve all values as strings": { + "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" + } + } + } + }, + "Pretty print (formatted output)": { + "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": { + "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.)": { + "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" + } + } + } + }, + "Preview Schema Changes": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xem trước thay đổi cấu trúc" + } + } + } + }, + "Previous Page (⌘[)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Trang trước (⌘[)" + } + } + } + }, + "Previous Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tab trước" + } + } + } + }, + "Private Key": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khóa riêng tư" + } + } + } + }, + "Purple": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tím" + } + } + } + }, + "Put field names in the first row": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đặt tên trường ở dòng đầu tiên" + } + } + } + }, + "Query": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Truy vấn" + } + } + } + }, + "Query executed successfully": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Truy vấn thực thi thành công" + } + } + } + }, + "Query executing...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đang thực thi truy vấn..." + } + } + } + }, + "Query Execution": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thực thi truy vấn" + } + } + } + }, + "Query Execution Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thực thi truy vấn thất bại" + } + } + } + }, + "Query timeout:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thời gian chờ truy vấn:" + } + } + } + }, + "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..." + } + } + } + }, + "Quote": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dấu ngoặc kép" + } + } + } + }, + "Quote if needed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đặt trong ngoặc kép nếu cần" + } + } + } + }, + "Raw SQL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SQL thô" + } + } + } + }, + "Raw SQL cannot be empty": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SQL thô không được để trống" + } + } + } + }, + "Read-only": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chỉ đọc" + } + } + } + }, + "Read-only connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối chỉ đọc" + } + } + } + }, + "RECENT": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "GẦN ĐÂY" + } + } + } + }, + "Red": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đỏ" + } + } + } + }, + "Redo": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Làm lại" + } + } + } + }, + "Referenced columns (comma-separated)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cột tham chiếu (phân tách bằng dấu phẩy)" + } + } + } + }, + "Referenced table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bảng tham chiếu" + } + } + } + }, + "Refresh": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Làm mới" + } + } + } + }, + "Refresh (⌘R)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Làm mới (⌘R)" + } + } + } + }, + "Refresh database list": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Làm mới danh sách cơ sở dữ liệ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." + } + } + } + }, + "Remove Filter": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xóa bộ lọc" + } + } + } + }, + "Remove license from this machine": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Gỡ giấy phép khỏi máy này" + } + } + } + }, + "Reopen Last Session": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mở lại phiên làm việc trước" + } + } + } + }, + "Replication lag: %llds": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ trễ sao chép: %llds" + } + } + } + }, + "Require SSL, skip verification": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Yêu cầu SSL, bỏ qua xác minh" + } + } + } + }, + "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." + } + } + } + }, + "Retention": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu giữ" + } + } + } + }, + "Retry": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thử lại" + } + } + } + }, + "root": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "root" + } + } + } + }, + "Row Details": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chi tiết dòng" + } + } + } + }, + "Row height:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chiều cao dòng:" + } + } + } + }, + "Rows": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dòng" + } + } + } + }, + "Rows per INSERT": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dòng mỗi INSERT" + } + } + } + }, + "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" + } + } + } + }, + "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." + } + } + } + }, + "Sanitize formula-like values": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Làm sạch giá trị giống công thức" + } + } + } + }, + "Save": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu" + } + } + } + }, + "Save and load filter presets": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu và tải mẫu bộ lọc" + } + } + } + }, + "Save as Preset...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu dưới dạng mẫu..." + } + } + } + }, + "Save as Template": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu dưới dạng mẫu" + } + } + } + }, + "Save Changes": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu thay đổi" + } + } + } + }, + "Save Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu thất bại" + } + } + } + }, + "Save Filter Preset": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu mẫu bộ lọc" + } + } + } + }, + "Save Table Template": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lưu mẫu bảng" + } + } + } + }, + "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" + } + } + } + }, + "Scale": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tỉ lệ" + } + } + } + }, + "Scale:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tỉ lệ:" + } + } + } + }, + "Search databases...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tìm cơ sở dữ liệu..." + } + } + } + }, + "Search for connection...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tìm kết nối..." + } + } + } + }, + "Search...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tìm kiếm..." + } + } + } + }, + "Second value is required for BETWEEN": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giá trị thứ hai là bắt buộc cho BETWEEN" + } + } + } + }, + "SELECT * FROM users WHERE id = 1;": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SELECT * FROM users WHERE id = 1;" + } + } + } + }, + "Select a table to copy its structure:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chọn bảng để sao chép cấu trúc:" + } + } + } + }, + "Select All": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chọn tất cả" + } + } + } + }, + "Select SQL File...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chọn tệp SQL..." + } + } + } + }, + "Select Tab %lld": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chọn tab %lld" + } + } + } + }, + "Set DEFAULT": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đặt DEFAULT" + } + } + } + }, + "Set NULL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đặt NULL" + } + } + } + }, + "Set special value": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đặt giá trị đặc biệt" + } + } + } + }, + "Set Value": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đặt giá trị" + } + } + } + }, + "Show alternate row backgrounds": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện nền xen kẽ dòng" + } + } + } + }, + "Show line numbers": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện số dòng" + } + } + } + }, + "Show Next Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện tab tiếp" + } + } + } + }, + "Show Previous Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện tab trước" + } + } + } + }, + "Show Structure": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện cấu trúc" + } + } + } + }, + "Show Welcome Screen": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện màn hình chào mừng" + } + } + } + }, + "Show Welcome Window": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hiện cửa sổ chào mừng" + } + } + } + }, + "SIZE": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "KÍCH THƯỚC" + } + } + } + }, + "Size:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kích thước:" + } + } + } + }, + "Software Update": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cập nhật phần mềm" + } + } + } + }, + "Spacious": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Rộng rãi" + } + } + } + }, + "SQL": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SQL" + } + } + } + }, + "SQL Functions": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hàm SQL" + } + } + } + }, + "SQLite is file-based": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SQLite dựa trên tệp" + } + } + } + }, + "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 command not found. Please ensure OpenSSH is installed.": { + "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 connection timed out": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kết nối SSH đã hết thời gian" + } + } + } + }, + "SSH Tunnel": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đường hầm 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 creation failed: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạo đường hầm SSH thất bại: %@" + } + } + } + }, + "ssh.example.com": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "ssh.example.com" + } + } + } + }, + "SSL/TLS": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "SSL/TLS" + } + } + } + }, + "starts with": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "bắt đầu bằng" + } + } + } + }, + "Statement %lld": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Câu lệnh %lld" + } + } + } + }, + "Statement %lld of %lld": { + "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" + } + } + } + }, + "Statement:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Câu lệnh:" + } + } + } + }, + "STATISTICS": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "THỐNG KÊ" + } + } + } + }, + "Stop": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Dừng" + } + } + } + }, + "Structure": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cấu trúc" + } + } + } + }, + "Structure, Drop, and Data options are configured per table in the table list.": { + "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." + } + } + } + }, + "Success": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thành công" + } + } + } + }, + "Suspended": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tạm ngưng" + } + } + } + }, + "Switch Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển kết nối" + } + } + } + }, + "Switch Database": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển cơ sở dữ liệu" + } + } + } + }, + "Switch Database (⌘K)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chuyển cơ sở dữ liệu (⌘K)" + } + } + } + }, + "System": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hệ thống" + } + } + } + }, + "Tab width:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Độ rộng tab:" + } + } + } + }, + "Table '%@' has no columns or does not exist": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bảng '%@' không có cột hoặc không tồn tại" + } + } + } + }, + "Table creation options not available": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tùy chọn tạo bảng không khả dụng" + } + } + } + }, + "Table Info": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thông tin bảng" + } + } + } + }, + "Table Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên bảng" + } + } + } + }, + "TablePro": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "TablePro" + } + } + } + }, + "Tables": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bảng" + } + } + } + }, + "Tablespace": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tablespace" + } + } + } + }, + "Tag name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên thẻ" + } + } + } + }, + "Tag: %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thẻ: %@" + } + } + } + }, + "Template": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mẫu" + } + } + } + }, + "Template Name": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tên mẫu" + } + } + } + }, + "Temporarily disable foreign key constraints during import. Useful for importing data with circular dependencies.": { + "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." + } + } + } + }, + "Test Connection": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Kiểm tra kết nối" + } + } + } + }, + "This database has no tables yet.": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Cơ sở dữ liệu này chưa có bảng nào." + } + } + } + }, + "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 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 operation is not supported": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Thao tác này không được hỗ trợ" + } + } + } + }, + "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 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 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." + } + } + } + }, + "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 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." + } + } + } + }, + "TIMESTAMPS": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "THỜI GIAN" + } + } + } + }, + "to view data": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "để xem dữ liệu" + } + } + } + }, + "Toggle Filters": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt bộ lọc" + } + } + } + }, + "Toggle Filters (⌘F)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt bộ lọc (⌘F)" + } + } + } + }, + "Toggle Filters (Cmd+F)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt bộ lọc (Cmd+F)" + } + } + } + }, + "Toggle History": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt lịch sử" + } + } + } + }, + "Toggle Inspector": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt thanh kiểm tra" + } + } + } + }, + "Toggle Inspector (⌘⌥B)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt thanh kiểm tra (⌘⌥B)" + } + } + } + }, + "Toggle Query History (⌘⇧H)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt lịch sử truy vấn (⌘⇧H)" + } + } + } + }, + "Toggle Table Browser": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bật/tắt trình duyệt bảng" + } + } + } + }, + "Total Size": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tổng kích thước" + } + } + } + }, + "TRUE": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "TRUE" + } + } + } + }, + "Truncate": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Truncate" + } + } + } + }, + "Truncate Table": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Truncate bảng" + } + } + } + }, + "Undo": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hoàn tác" + } + } + } + }, + "Undo Delete": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Hoàn tác xóa" + } + } + } + }, + "Unique": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Duy nhất" + } + } + } + }, + "UNIQUE": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "UNIQUE" + } + } + } + }, + "Unknown error": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Lỗi không xác định" + } + } + } + }, + "Unlicensed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chưa có giấy phép" + } + } + } + }, + "Unlimited": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không giới hạn" + } + } + } + }, + "Unpin Tab": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bỏ ghim tab" + } + } + } + }, + "Unset": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bỏ đặt" + } + } + } + }, + "Unsigned": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Không dấu" + } + } + } + }, + "Updated": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Đã cập nhật" + } + } + } + }, + "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 Short (12/31/2024)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mỹ ngắn (12/31/2024)" + } + } + } + }, + "Use SSL if available": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Sử dụng SSL nếu có" + } + } + } + }, + "username": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "username" + } + } + } + }, + "UTC_TIMESTAMP()": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "UTC_TIMESTAMP()" + } + } + } + }, + "Validation Failed": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xác thực thất bại" + } + } + } + }, + "Value": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giá trị" + } + } + } + }, + "Value is required": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Giá trị là bắt buộc" + } + } + } + }, + "Verify certificate and hostname": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xác minh chứng chỉ và tên máy chủ" + } + } + } + }, + "Verify server certificate": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Xác minh chứng chỉ máy chủ" + } + } + } + }, + "Version %@": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Phiên bản %@" + } + } + } + }, + "WARNING: Failed to re-enable foreign key checks: %@. Please manually verify FK constraints are enabled.": { + "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." + } + } + } + }, + "Welcome to TablePro": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Chào mừng đến với TablePro" + } + } + } + }, + "When TablePro starts:": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Khi TablePro khởi động:" + } + } + } + }, + "WHERE clause...": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Mệnh đề WHERE..." + } + } + } + }, + "Word wrap": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Tự động xuống dòng" + } + } + } + }, + "Wrap in transaction (BEGIN/COMMIT)": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Bọc trong giao dịch (BEGIN/COMMIT)" + } + } + } + }, + "Yellow": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Vàng" + } + } + } + }, + "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" + } + } + } + }, + "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." + } + } + } + }, + "Zero Fill": { + "localizations": { + "vi": { + "stringUnit": { + "state": "translated", + "value": "Điền số 0" + } + } + } + } + }, + "version": "1.0" +} diff --git a/TablePro/Views/Components/EmptyStateView.swift b/TablePro/Views/Components/EmptyStateView.swift index bcb293adf..a8eff6dc3 100644 --- a/TablePro/Views/Components/EmptyStateView.swift +++ b/TablePro/Views/Components/EmptyStateView.swift @@ -77,9 +77,9 @@ extension EmptyStateView { static func foreignKeys(onAdd: @escaping () -> Void) -> EmptyStateView { EmptyStateView( icon: "link", - title: "No Foreign Keys Yet", - description: "Click + to add a relationship between this table and another", - actionTitle: "Add Foreign Key", + title: String(localized: "No Foreign Keys Yet"), + description: String(localized: "Click + to add a relationship between this table and another"), + actionTitle: String(localized: "Add Foreign Key"), action: onAdd ) } @@ -88,9 +88,9 @@ extension EmptyStateView { static func indexes(onAdd: @escaping () -> Void) -> EmptyStateView { EmptyStateView( icon: "list.bullet.indent", - title: "No Indexes Defined", - description: "Add indexes to improve query performance on frequently searched columns", - actionTitle: "Add Index", + title: String(localized: "No Indexes Defined"), + description: String(localized: "Add indexes to improve query performance on frequently searched columns"), + actionTitle: String(localized: "Add Index"), action: onAdd ) } @@ -99,9 +99,9 @@ extension EmptyStateView { static func checkConstraints(onAdd: @escaping () -> Void) -> EmptyStateView { EmptyStateView( icon: "checkmark.shield", - title: "No Check Constraints", - description: "Add validation rules to ensure data integrity", - actionTitle: "Add Check Constraint", + title: String(localized: "No Check Constraints"), + description: String(localized: "Add validation rules to ensure data integrity"), + actionTitle: String(localized: "Add Check Constraint"), action: onAdd ) } @@ -110,9 +110,9 @@ extension EmptyStateView { static func columns(onAdd: @escaping () -> Void) -> EmptyStateView { EmptyStateView( icon: "tablecells", - title: "No Columns Defined", - description: "Every table needs at least one column. Click + to get started", - actionTitle: "Add Column", + title: String(localized: "No Columns Defined"), + description: String(localized: "Every table needs at least one column. Click + to get started"), + actionTitle: String(localized: "Add Column"), action: onAdd ) } diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index bf5a955af..7ccbb4a43 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -72,7 +72,7 @@ struct ConnectionFormView: View { // Header HStack { Spacer() - Text(isNew ? "New Connection" : "Edit Connection") + Text(isNew ? String(localized: "New Connection") : String(localized: "Edit Connection")) .font(.headline) Spacer() } @@ -197,7 +197,7 @@ struct ConnectionFormView: View { } FormField( - label: type == .sqlite ? "File Path" : "Database", + label: type == .sqlite ? String(localized: "File Path") : String(localized: "Database"), icon: type == .sqlite ? "doc" : "cylinder" ) { HStack { @@ -468,7 +468,7 @@ struct ConnectionFormView: View { } // Save - Button(isNew ? "Create" : "Save") { + Button(isNew ? String(localized: "Create") : String(localized: "Save")) { saveConnection() } .keyboardShortcut(.return) @@ -712,7 +712,7 @@ struct ConnectionFormView: View { testConn, sshPassword: sshPassword) await MainActor.run { isTesting = false - testResult = success ? .success : .failure("Connection test failed") + testResult = success ? .success : .failure(String(localized: "Connection test failed")) } } catch { await MainActor.run { diff --git a/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift b/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift index 4b646b4cd..4aa762c0c 100644 --- a/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift +++ b/TablePro/Views/DatabaseSwitcher/CreateDatabaseSheet.swift @@ -105,7 +105,7 @@ struct CreateDatabaseSheet: View { Spacer() - Button(isCreating ? "Creating..." : "Create") { + Button(isCreating ? String(localized: "Creating...") : String(localized: "Create")) { createDatabase() } .buttonStyle(.borderedProminent) diff --git a/TablePro/Views/Editor/CreateTableView.swift b/TablePro/Views/Editor/CreateTableView.swift index 78ca5ccc6..e05e90d7f 100644 --- a/TablePro/Views/Editor/CreateTableView.swift +++ b/TablePro/Views/Editor/CreateTableView.swift @@ -648,7 +648,7 @@ struct CreateTableView: View { try TableTemplateStorage.shared.saveTemplate(name: templateName, options: options) templateName = "" } catch { - validationError = "Failed to save template: \(error.localizedDescription)" + validationError = String(localized: "Failed to save template: \(error.localizedDescription)") } } @@ -663,7 +663,7 @@ struct CreateTableView: View { selectedColumnId = options.columns.first?.id } } catch { - validationError = "Failed to load template: \(error.localizedDescription)" + validationError = String(localized: "Failed to load template: \(error.localizedDescription)") } } @@ -672,7 +672,7 @@ struct CreateTableView: View { try TableTemplateStorage.shared.deleteTemplate(name: name) savedTemplates = TableTemplateStorage.shared.getTemplateNames() } catch { - validationError = "Failed to delete template: \(error.localizedDescription)" + validationError = String(localized: "Failed to delete template: \(error.localizedDescription)") } } @@ -685,7 +685,7 @@ struct CreateTableView: View { selectedColumnId = options.columns.first?.id ddlText = "" } catch { - validationError = "Failed to import DDL: \(error.localizedDescription)" + validationError = String(localized: "Failed to import DDL: \(error.localizedDescription)") } } @@ -704,7 +704,7 @@ struct CreateTableView: View { } } catch { await MainActor.run { - validationError = "Failed to load tables: \(error.localizedDescription)" + validationError = String(localized: "Failed to load tables: \(error.localizedDescription)") } } } @@ -715,7 +715,7 @@ struct CreateTableView: View { do { guard let driver = DatabaseManager.shared.activeDriver else { await MainActor.run { - validationError = "No database connection" + validationError = String(localized: "No database connection") } return } @@ -764,7 +764,7 @@ struct CreateTableView: View { await MainActor.run { guard !result.rows.isEmpty else { - validationError = "Table '\(tableName)' has no columns or does not exist" + validationError = String(localized: "Table '\(tableName)' has no columns or does not exist") return } @@ -866,7 +866,7 @@ struct CreateTableView: View { Self.logger.debug("Primary keys = \(newPrimaryKeys.description, privacy: .public)") guard !newColumns.isEmpty else { - validationError = "Failed to parse any columns from table '\(tableName)'. Check console for debug info." + validationError = String(localized: "Failed to parse any columns from table '\(tableName)'. Check console for debug info.") return } @@ -896,7 +896,7 @@ struct CreateTableView: View { } } catch { await MainActor.run { - validationError = "Failed to fetch table structure: \(error.localizedDescription)" + validationError = String(localized: "Failed to fetch table structure: \(error.localizedDescription)") } } } diff --git a/TablePro/Views/Editor/EditorTabBar.swift b/TablePro/Views/Editor/EditorTabBar.swift index 6255dc359..3b79011b4 100644 --- a/TablePro/Views/Editor/EditorTabBar.swift +++ b/TablePro/Views/Editor/EditorTabBar.swift @@ -47,23 +47,23 @@ struct EditorTabBar: View { @ViewBuilder private func tabContextMenu(for tab: QueryTab) -> some View { - Button("Duplicate Tab") { + Button(String(localized: "Duplicate Tab")) { tabManager.duplicateTab(tab) } - Button(tab.isPinned ? "Unpin Tab" : "Pin Tab") { + Button(tab.isPinned ? String(localized: "Unpin Tab") : String(localized: "Pin Tab")) { tabManager.togglePin(tab) } Divider() if !tab.isPinned { - Button("Close Tab") { + Button(String(localized: "Close Tab")) { tabManager.closeTab(tab) } } - Button("Close Other Tabs") { + Button(String(localized: "Close Other Tabs")) { let kept = tabManager.tabs.filter { $0.id == tab.id || $0.isPinned } tabManager.tabs = kept.isEmpty ? [] : kept tabManager.selectedTabId = tab.id diff --git a/TablePro/Views/Editor/QuerySuccessView.swift b/TablePro/Views/Editor/QuerySuccessView.swift index 854ac8f71..4e4eaeeea 100644 --- a/TablePro/Views/Editor/QuerySuccessView.swift +++ b/TablePro/Views/Editor/QuerySuccessView.swift @@ -51,11 +51,14 @@ struct QuerySuccessView: View { private func formatExecutionTime(_ time: TimeInterval) -> String { if time < 0.001 { - return String(format: "%.3f ms", time * 1_000) + let ms = String(format: "%.3f", time * 1_000) + return String(localized: "\(ms) ms") } else if time < 1 { - return String(format: "%.2f ms", time * 1_000) + let ms = String(format: "%.2f", time * 1_000) + return String(localized: "\(ms) ms") } else { - return String(format: "%.2f s", time) + let secs = String(format: "%.2f", time) + return String(localized: "\(secs) s") } } } diff --git a/TablePro/Views/Export/ExportCSVOptionsView.swift b/TablePro/Views/Export/ExportCSVOptionsView.swift index fbac5b1a7..70e010b5f 100644 --- a/TablePro/Views/Export/ExportCSVOptionsView.swift +++ b/TablePro/Views/Export/ExportCSVOptionsView.swift @@ -35,7 +35,7 @@ struct ExportCSVOptionsView: View { // Dropdowns section VStack(alignment: .leading, spacing: 10) { - optionRow("Delimiter") { + optionRow(String(localized: "Delimiter")) { Picker("", selection: $options.delimiter) { ForEach(CSVDelimiter.allCases) { delimiter in Text(delimiter.displayName).tag(delimiter) @@ -46,7 +46,7 @@ struct ExportCSVOptionsView: View { .frame(width: 140, alignment: .trailing) } - optionRow("Quote") { + optionRow(String(localized: "Quote")) { Picker("", selection: $options.quoteHandling) { ForEach(CSVQuoteHandling.allCases) { handling in Text(handling.rawValue).tag(handling) @@ -57,7 +57,7 @@ struct ExportCSVOptionsView: View { .frame(width: 140, alignment: .trailing) } - optionRow("Line break") { + optionRow(String(localized: "Line break")) { Picker("", selection: $options.lineBreak) { ForEach(CSVLineBreak.allCases) { lineBreak in Text(lineBreak.rawValue).tag(lineBreak) @@ -68,7 +68,7 @@ struct ExportCSVOptionsView: View { .frame(width: 140, alignment: .trailing) } - optionRow("Decimal") { + optionRow(String(localized: "Decimal")) { Picker("", selection: $options.decimalFormat) { ForEach(CSVDecimalFormat.allCases) { format in Text(format.rawValue).tag(format) diff --git a/TablePro/Views/Export/ExportDialog.swift b/TablePro/Views/Export/ExportDialog.swift index 78ec15d45..66608fb1b 100644 --- a/TablePro/Views/Export/ExportDialog.swift +++ b/TablePro/Views/Export/ExportDialog.swift @@ -381,8 +381,8 @@ struct ExportDialog: View { guard let driver = DatabaseManager.shared.activeDriver else { isLoading = false AlertHelper.showErrorSheet( - title: "Export Error", - message: "Not connected to database", + title: String(localized: "Export Error"), + message: String(localized: "Not connected to database"), window: nil ) return @@ -480,8 +480,8 @@ struct ExportDialog: View { } catch { isLoading = false AlertHelper.showErrorSheet( - title: "Export Error", - message: "Failed to load databases: \(error.localizedDescription)", + title: String(localized: "Export Error"), + message: String(localized: "Failed to load databases: \(error.localizedDescription)"), window: nil ) } @@ -575,8 +575,8 @@ struct ExportDialog: View { private func startExport(to url: URL) async { guard let driver = DatabaseManager.shared.activeDriver else { AlertHelper.showErrorSheet( - title: "Export Error", - message: "Not connected to database", + title: String(localized: "Export Error"), + message: String(localized: "Not connected to database"), window: nil ) return @@ -615,7 +615,7 @@ struct ExportDialog: View { showProgressDialog = false isExporting = false AlertHelper.showErrorSheet( - title: "Export Error", + title: String(localized: "Export Error"), message: error.localizedDescription, window: nil ) diff --git a/TablePro/Views/Export/ExportProgressView.swift b/TablePro/Views/Export/ExportProgressView.swift index 00ce718d8..7e21c72b2 100644 --- a/TablePro/Views/Export/ExportProgressView.swift +++ b/TablePro/Views/Export/ExportProgressView.swift @@ -21,7 +21,9 @@ struct ExportProgressView: View { var body: some View { VStack(spacing: 20) { // Title - Text(totalTables > 1 ? "Export multiple tables" : "Export table") + Text(totalTables > 1 + ? String(localized: "Export multiple tables") + : String(localized: "Export table")) .font(.system(size: 15, weight: .semibold)) // Table info and row count diff --git a/TablePro/Views/Export/ExportSQLOptionsView.swift b/TablePro/Views/Export/ExportSQLOptionsView.swift index 1c4200083..8a8f55ee1 100644 --- a/TablePro/Views/Export/ExportSQLOptionsView.swift +++ b/TablePro/Views/Export/ExportSQLOptionsView.swift @@ -36,7 +36,7 @@ struct ExportSQLOptionsView: View { Picker("", selection: $options.batchSize) { ForEach(Self.batchSizeOptions, id: \.self) { size in - Text(size == 1 ? "1 (no batching)" : "\(size)") + Text(size == 1 ? String(localized: "1 (no batching)") : "\(size)") .tag(size) } } diff --git a/TablePro/Views/Export/ExportTableOutlineView.swift b/TablePro/Views/Export/ExportTableOutlineView.swift index e90e060d7..150b675ee 100644 --- a/TablePro/Views/Export/ExportTableOutlineView.swift +++ b/TablePro/Views/Export/ExportTableOutlineView.swift @@ -98,7 +98,7 @@ struct ExportTableOutlineView: NSViewRepresentable { // SQL format: Name + 3 option columns // Total: 165 + 142 = 307px (prioritizes readability, allows scrolling) let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) - nameColumn.title = "Name" + nameColumn.title = String(localized: "Name") nameColumn.width = 165 nameColumn.minWidth = 165 nameColumn.maxWidth = 165 @@ -106,21 +106,21 @@ struct ExportTableOutlineView: NSViewRepresentable { outlineView.outlineTableColumn = nameColumn let structureColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("structure")) - structureColumn.title = "Structure" + structureColumn.title = String(localized: "Structure") structureColumn.width = 54 structureColumn.minWidth = 54 structureColumn.maxWidth = 54 outlineView.addTableColumn(structureColumn) let dropColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("drop")) - dropColumn.title = "Drop" + dropColumn.title = String(localized: "Drop") dropColumn.width = 44 dropColumn.minWidth = 44 dropColumn.maxWidth = 44 outlineView.addTableColumn(dropColumn) let dataColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("data")) - dataColumn.title = "Data" + dataColumn.title = String(localized: "Data") dataColumn.width = 44 dataColumn.minWidth = 44 dataColumn.maxWidth = 44 @@ -128,7 +128,7 @@ struct ExportTableOutlineView: NSViewRepresentable { } else { // CSV/JSON format: Single name column, truncates long names let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) - nameColumn.title = "Name" + nameColumn.title = String(localized: "Name") nameColumn.width = 200 nameColumn.minWidth = 200 nameColumn.maxWidth = 200 diff --git a/TablePro/Views/Filter/FilterRowView.swift b/TablePro/Views/Filter/FilterRowView.swift index 26845ed5f..8846f36f1 100644 --- a/TablePro/Views/Filter/FilterRowView.swift +++ b/TablePro/Views/Filter/FilterRowView.swift @@ -23,9 +23,9 @@ struct FilterRowView: View { /// Display name for the column (handles raw SQL and empty) private var displayColumnName: String { if filter.columnName == TableFilter.rawSQLColumn { - return "Raw SQL" + return String(localized: "Raw SQL") } else if filter.columnName.isEmpty { - return "Column" + return String(localized: "Column") } else { return filter.columnName } diff --git a/TablePro/Views/History/HistoryListViewController.swift b/TablePro/Views/History/HistoryListViewController.swift index 3f13b1aa7..678052017 100644 --- a/TablePro/Views/History/HistoryListViewController.swift +++ b/TablePro/Views/History/HistoryListViewController.swift @@ -366,11 +366,11 @@ final class HistoryListViewController: NSViewController, NSMenuItemValidation { Task { @MainActor in let confirmed = await AlertHelper.confirmDestructive( - title: "Clear All History?", + title: String(localized: "Clear All History?"), message: - "This will permanently delete \(count) \(itemName). This action cannot be undone.", - confirmButton: "Clear All", - cancelButton: "Cancel" + String(localized: "This will permanently delete \(count) \(itemName). This action cannot be undone."), + confirmButton: String(localized: "Clear All"), + cancelButton: String(localized: "Cancel") ) if confirmed { diff --git a/TablePro/Views/Import/ImportDialog.swift b/TablePro/Views/Import/ImportDialog.swift index 2d85c269c..5d969d33f 100644 --- a/TablePro/Views/Import/ImportDialog.swift +++ b/TablePro/Views/Import/ImportDialog.swift @@ -120,7 +120,7 @@ struct ImportDialog: View { // MARK: - View Components private var fileSelectionView: some View { - Button(fileURL == nil ? "Select SQL File..." : "Change File") { + Button(fileURL == nil ? String(localized: "Select SQL File...") : String(localized: "Change File")) { selectFile() } .buttonStyle(.borderedProminent) @@ -290,7 +290,7 @@ struct ImportDialog: View { guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory), !isDirectory.boolValue else { - filePreview = "Error: Selected path is not a regular file" + filePreview = String(localized: "Error: Selected path is not a regular file") return } @@ -314,7 +314,7 @@ struct ImportDialog: View { tempPreviewURL = urlToRead } } catch { - filePreview = "Failed to decompress file: \(error.localizedDescription)" + filePreview = String(localized: "Failed to decompress file: \(error.localizedDescription)") return } @@ -337,13 +337,10 @@ struct ImportDialog: View { filePreview = preview } else { let encodingDescription = String(describing: config.encoding) - filePreview = """ - Failed to load preview using encoding: \(encodingDescription). - Try selecting a different text encoding from the encoding picker and reload the preview. - """ + filePreview = String(localized: "Failed to load preview using encoding: \(encodingDescription). Try selecting a different text encoding from the encoding picker and reload the preview.") } } catch { - filePreview = "Failed to load preview: \(error.localizedDescription)" + filePreview = String(localized: "Failed to load preview: \(error.localizedDescription)") } // Count statements asynchronously diff --git a/TablePro/Views/Import/ImportErrorView.swift b/TablePro/Views/Import/ImportErrorView.swift index bebd89763..e1ece6406 100644 --- a/TablePro/Views/Import/ImportErrorView.swift +++ b/TablePro/Views/Import/ImportErrorView.swift @@ -50,7 +50,7 @@ struct ImportErrorView: View { .background(Color(nsColor: .textBackgroundColor)) .cornerRadius(4) } else { - Text(error?.localizedDescription ?? "Unknown error") + Text(error?.localizedDescription ?? String(localized: "Unknown error")) .font(.system(size: 13)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) diff --git a/TablePro/Views/Import/ImportSuccessView.swift b/TablePro/Views/Import/ImportSuccessView.swift index c3ff19967..4a2f2efa6 100644 --- a/TablePro/Views/Import/ImportSuccessView.swift +++ b/TablePro/Views/Import/ImportSuccessView.swift @@ -26,7 +26,8 @@ struct ImportSuccessView: View { .font(.system(size: 13)) .foregroundStyle(.secondary) - Text(String(format: "%.2f seconds", result.executionTime)) + let formattedTime = String(format: "%.2f", result.executionTime) + Text(String(localized: "\(formattedTime) seconds")) .font(.system(size: 12)) .foregroundStyle(.secondary) } diff --git a/TablePro/Views/Main/Child/MainStatusBarView.swift b/TablePro/Views/Main/Child/MainStatusBarView.swift index da3a78721..9b02d4934 100644 --- a/TablePro/Views/Main/Child/MainStatusBarView.swift +++ b/TablePro/Views/Main/Child/MainStatusBarView.swift @@ -105,9 +105,9 @@ struct MainStatusBarView: View { if selectedCount > 0 { // Selection mode: "5 of 200 rows selected" if selectedCount == loadedCount { - return "All \(loadedCount) rows selected" + return String(localized: "All \(loadedCount) rows selected") } else { - return "\(selectedCount) of \(loadedCount) rows selected" + return String(localized: "\(selectedCount) of \(loadedCount) rows selected") } } else if tab.tabType == .table, let total = total, total > 0 { // Pagination mode (table tabs only): "201-400 of 5,000 rows" @@ -115,14 +115,14 @@ struct MainStatusBarView: View { formatter.numberStyle = .decimal let formattedTotal = formatter.string(from: NSNumber(value: total)) ?? "\(total)" - return "\(pagination.rangeStart)-\(pagination.rangeEnd) of \(formattedTotal) rows" + return String(localized: "\(pagination.rangeStart)-\(pagination.rangeEnd) of \(formattedTotal) rows") } else if loadedCount > 0 { let formatter = NumberFormatter() formatter.numberStyle = .decimal let formattedCount = formatter.string(from: NSNumber(value: loadedCount)) ?? "\(loadedCount)" - return "\(formattedCount) rows" + return String(localized: "\(formattedCount) rows") } else { - return "No rows" + return String(localized: "No rows") } } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Alerts.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Alerts.swift index 78a884a56..9999af998 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Alerts.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Alerts.swift @@ -20,10 +20,10 @@ extension MainContentCoordinator { let message = dangerousQueryMessage(for: sql) return await AlertHelper.confirmCritical( - title: "Potentially Dangerous Query", + title: String(localized: "Potentially Dangerous Query"), message: message, - confirmButton: "Execute", - cancelButton: "Cancel" + confirmButton: String(localized: "Execute"), + cancelButton: String(localized: "Cancel") ) } @@ -32,14 +32,14 @@ extension MainContentCoordinator { let uppercased = sql.uppercased().trimmingCharacters(in: .whitespacesAndNewlines) if uppercased.hasPrefix("DROP ") { - return "This DROP query will permanently remove database objects. This action cannot be undone." + return String(localized: "This DROP query will permanently remove database objects. This action cannot be undone.") } else if uppercased.hasPrefix("TRUNCATE ") { - return "This TRUNCATE query will permanently delete all rows in the table. This action cannot be undone." + return String(localized: "This TRUNCATE query will permanently delete all rows in the table. This action cannot be undone.") } else if uppercased.hasPrefix("DELETE ") { - return "This DELETE query has no WHERE clause and will delete ALL rows in the table. This action cannot be undone." + return String(localized: "This DELETE query has no WHERE clause and will delete ALL rows in the table. This action cannot be undone.") } - return "This query may permanently modify or delete data." + return String(localized: "This query may permanently modify or delete data.") } // MARK: - Discard Changes Confirmation @@ -50,10 +50,10 @@ extension MainContentCoordinator { func confirmDiscardChanges(action: DiscardAction) async -> Bool { let message = discardMessage(for: action) return await AlertHelper.confirmDestructive( - title: "Discard Unsaved Changes?", + title: String(localized: "Discard Unsaved Changes?"), message: message, - confirmButton: "Discard", - cancelButton: "Cancel" + confirmButton: String(localized: "Discard"), + cancelButton: String(localized: "Cancel") ) } @@ -61,9 +61,9 @@ extension MainContentCoordinator { private func discardMessage(for action: DiscardAction) -> String { switch action { case .refresh, .refreshAll: - return "Refreshing will discard all unsaved changes." + return String(localized: "Refreshing will discard all unsaved changes.") case .closeTab: - return "Closing this tab will discard all unsaved changes." + return String(localized: "Closing this tab will discard all unsaved changes.") } } @@ -75,7 +75,7 @@ extension MainContentCoordinator { /// - window: Parent window (optional) func showQueryError(_ error: Error, window: NSWindow?) { AlertHelper.showErrorSheet( - title: "Query Execution Failed", + title: String(localized: "Query Execution Failed"), message: error.localizedDescription, window: window ) @@ -87,7 +87,7 @@ extension MainContentCoordinator { /// - window: Parent window (optional) func showSaveError(_ error: Error, window: NSWindow?) { AlertHelper.showErrorSheet( - title: "Failed to Save Changes", + title: String(localized: "Failed to Save Changes"), message: error.localizedDescription, window: window ) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift index 4158a6b93..22b24120d 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift @@ -285,7 +285,7 @@ extension MainContentCoordinator { ) AlertHelper.showErrorSheet( - title: "Query Execution Failed", + title: String(localized: "Query Execution Failed"), message: contextMsg, window: NSApp.keyWindow ) diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 63b3b484e..50ec1b9dd 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -482,7 +482,7 @@ final class MainContentCoordinator: ObservableObject { // Show error alert to user AlertHelper.showErrorSheet( - title: "Query Execution Failed", + title: String(localized: "Query Execution Failed"), message: error.localizedDescription, window: NSApp.keyWindow ) @@ -1116,7 +1116,7 @@ final class MainContentCoordinator: ObservableObject { // Show error alert to user AlertHelper.showErrorSheet( - title: "Save Failed", + title: String(localized: "Save Failed"), message: error.localizedDescription, window: NSApplication.shared.keyWindow ) diff --git a/TablePro/Views/MainContentView.swift b/TablePro/Views/MainContentView.swift index be5c6d5c3..f04100aef 100644 --- a/TablePro/Views/MainContentView.swift +++ b/TablePro/Views/MainContentView.swift @@ -444,7 +444,7 @@ struct MainContentView: View { // Show error alert to user AlertHelper.showErrorSheet( - title: "Save Failed", + title: String(localized: "Save Failed"), message: error.localizedDescription, window: NSApplication.shared.keyWindow ) @@ -543,7 +543,7 @@ struct MainContentView: View { } catch { // Show error using macOS alert AlertHelper.showErrorSheet( - title: "Failed to Save Changes", + title: String(localized: "Failed to Save Changes"), message: error.localizedDescription, window: nil ) diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index 96164deb7..bf78ce53c 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -492,12 +492,12 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData let column = tableView.tableColumns[columnIndex] if column.identifier.rawValue == "__rowNumber__" { return } - let copyItem = NSMenuItem(title: "Copy Column Name", action: #selector(copyColumnName(_:)), keyEquivalent: "") + let copyItem = NSMenuItem(title: String(localized: "Copy Column Name"), action: #selector(copyColumnName(_:)), keyEquivalent: "") copyItem.representedObject = column.title copyItem.target = self menu.addItem(copyItem) - let filterItem = NSMenuItem(title: "Filter with column", action: #selector(filterWithColumn(_:)), keyEquivalent: "") + let filterItem = NSMenuItem(title: String(localized: "Filter with column"), action: #selector(filterWithColumn(_:)), keyEquivalent: "") filterItem.representedObject = column.title filterItem.target = self menu.addItem(filterItem) diff --git a/TablePro/Views/Results/ForeignKeyPopoverController.swift b/TablePro/Views/Results/ForeignKeyPopoverController.swift index 635fda3db..8535d5f54 100644 --- a/TablePro/Views/Results/ForeignKeyPopoverController.swift +++ b/TablePro/Views/Results/ForeignKeyPopoverController.swift @@ -94,7 +94,7 @@ final class ForeignKeyPopoverController: NSObject, NSPopoverDelegate { x: 8, y: height - 36, width: Self.popoverWidth - 16, height: 28 )) - search.placeholderString = "Search..." + search.placeholderString = String(localized: "Search...") search.font = .systemFont(ofSize: 13) search.target = self search.action = #selector(searchChanged) diff --git a/TablePro/Views/Results/TableRowViewWithMenu.swift b/TablePro/Views/Results/TableRowViewWithMenu.swift index 8f2071a68..c82a9f1db 100644 --- a/TablePro/Views/Results/TableRowViewWithMenu.swift +++ b/TablePro/Views/Results/TableRowViewWithMenu.swift @@ -29,7 +29,7 @@ final class TableRowViewWithMenu: NSTableRowView { if coordinator.changeManager.isRowDeleted(rowIndex) { menu.addItem( - withTitle: "Undo Delete", action: #selector(undoDeleteRow), keyEquivalent: "" + withTitle: String(localized: "Undo Delete"), action: #selector(undoDeleteRow), keyEquivalent: "" ).target = self } @@ -40,24 +40,24 @@ final class TableRowViewWithMenu: NSTableRowView { let setValueMenu = NSMenu() let emptyItem = NSMenuItem( - title: "Empty", action: #selector(setEmptyValue(_:)), keyEquivalent: "") + title: String(localized: "Empty"), action: #selector(setEmptyValue(_:)), keyEquivalent: "") emptyItem.representedObject = dataColumnIndex emptyItem.target = self setValueMenu.addItem(emptyItem) let nullItem = NSMenuItem( - title: "NULL", action: #selector(setNullValue(_:)), keyEquivalent: "") + title: String(localized: "NULL"), action: #selector(setNullValue(_:)), keyEquivalent: "") nullItem.representedObject = dataColumnIndex nullItem.target = self setValueMenu.addItem(nullItem) let defaultItem = NSMenuItem( - title: "Default", action: #selector(setDefaultValue(_:)), keyEquivalent: "") + title: String(localized: "Default"), action: #selector(setDefaultValue(_:)), keyEquivalent: "") defaultItem.representedObject = dataColumnIndex defaultItem.target = self setValueMenu.addItem(defaultItem) - let setValueItem = NSMenuItem(title: "Set Value", action: nil, keyEquivalent: "") + let setValueItem = NSMenuItem(title: String(localized: "Set Value"), action: nil, keyEquivalent: "") setValueItem.submenu = setValueMenu menu.addItem(setValueItem) @@ -67,7 +67,7 @@ final class TableRowViewWithMenu: NSTableRowView { // Copy actions if dataColumnIndex >= 0 { let copyCellItem = NSMenuItem( - title: "Copy Cell Value", action: #selector(copyCellValue(_:)), + title: String(localized: "Copy Cell Value"), action: #selector(copyCellValue(_:)), keyEquivalent: "") copyCellItem.representedObject = dataColumnIndex copyCellItem.target = self @@ -75,14 +75,14 @@ final class TableRowViewWithMenu: NSTableRowView { } let copyItem = NSMenuItem( - title: "Copy", action: #selector(copySelectedOrCurrentRow), keyEquivalent: "c") + title: String(localized: "Copy"), action: #selector(copySelectedOrCurrentRow), keyEquivalent: "c") copyItem.keyEquivalentModifierMask = .command copyItem.target = self menu.addItem(copyItem) if coordinator.isEditable { let pasteItem = NSMenuItem( - title: "Paste", action: #selector(pasteRows), keyEquivalent: "v") + title: String(localized: "Paste"), action: #selector(pasteRows), keyEquivalent: "v") pasteItem.keyEquivalentModifierMask = .command pasteItem.target = self menu.addItem(pasteItem) @@ -90,13 +90,13 @@ final class TableRowViewWithMenu: NSTableRowView { menu.addItem(NSMenuItem.separator()) let duplicateItem = NSMenuItem( - title: "Duplicate", action: #selector(duplicateRow), keyEquivalent: "d") + title: String(localized: "Duplicate"), action: #selector(duplicateRow), keyEquivalent: "d") duplicateItem.keyEquivalentModifierMask = .command duplicateItem.target = self menu.addItem(duplicateItem) let deleteItem = NSMenuItem( - title: "Delete", + title: String(localized: "Delete"), action: #selector(deleteRow), keyEquivalent: String(UnicodeScalar(NSBackspaceCharacter).map { Character($0) } ?? "\u{8}") ) diff --git a/TablePro/Views/RightSidebar/RightSidebarView.swift b/TablePro/Views/RightSidebar/RightSidebarView.swift index 6ad92541e..68a0e3aaa 100644 --- a/TablePro/Views/RightSidebar/RightSidebarView.swift +++ b/TablePro/Views/RightSidebar/RightSidebarView.swift @@ -22,9 +22,9 @@ struct RightSidebarView: View { private var mode: String { if selectedRowData != nil { - return isEditable ? "Edit Row" : "Row Details" + return isEditable ? String(localized: "Edit Row") : String(localized: "Row Details") } - return "Table Info" + return String(localized: "Table Info") } var body: some View { @@ -121,36 +121,36 @@ struct RightSidebarView: View { @ViewBuilder private func tableInfoContent(_ metadata: TableMetadata) -> some View { - sectionHeader("SIZE") - propertyRow("Data Size", TableMetadata.formatSize(metadata.dataSize)) - propertyRow("Index Size", TableMetadata.formatSize(metadata.indexSize)) - propertyRow("Total Size", TableMetadata.formatSize(metadata.totalSize)) + sectionHeader(String(localized: "SIZE")) + propertyRow(String(localized: "Data Size"), TableMetadata.formatSize(metadata.dataSize)) + propertyRow(String(localized: "Index Size"), TableMetadata.formatSize(metadata.indexSize)) + propertyRow(String(localized: "Total Size"), TableMetadata.formatSize(metadata.totalSize)) - sectionHeader("STATISTICS") + sectionHeader(String(localized: "STATISTICS")) if let rows = metadata.rowCount { - propertyRow("Rows", "\(rows)") + propertyRow(String(localized: "Rows"), "\(rows)") } if let avgLen = metadata.avgRowLength { - propertyRow("Avg Row", "\(avgLen) B") + propertyRow(String(localized: "Avg Row"), "\(avgLen) B") } if metadata.engine != nil || metadata.collation != nil { - sectionHeader("METADATA") + sectionHeader(String(localized: "METADATA")) if let engine = metadata.engine { - propertyRow("Engine", engine) + propertyRow(String(localized: "Engine"), engine) } if let collation = metadata.collation { - propertyRow("Collation", collation) + propertyRow(String(localized: "Collation"), collation) } } if metadata.createTime != nil || metadata.updateTime != nil { - sectionHeader("TIMESTAMPS") + sectionHeader(String(localized: "TIMESTAMPS")) if let create = metadata.createTime { - propertyRow("Created", formatDate(create)) + propertyRow(String(localized: "Created"), formatDate(create)) } if let update = metadata.updateTime { - propertyRow("Updated", formatDate(update)) + propertyRow(String(localized: "Updated"), formatDate(update)) } } } @@ -175,7 +175,7 @@ struct RightSidebarView: View { ($0.originalValue?.localizedCaseInsensitiveContains(searchText) ?? false) } - sectionHeader("FIELDS (\(filtered.count))") + sectionHeader(String(localized: "FIELDS (\(filtered.count))")) ForEach(filtered, id: \.columnName) { field in if isEditable && !isRowDeleted { diff --git a/TablePro/Views/Settings/GeneralSettingsView.swift b/TablePro/Views/Settings/GeneralSettingsView.swift index f60c71a90..abda853f1 100644 --- a/TablePro/Views/Settings/GeneralSettingsView.swift +++ b/TablePro/Views/Settings/GeneralSettingsView.swift @@ -11,9 +11,33 @@ import SwiftUI struct GeneralSettingsView: View { @Binding var settings: GeneralSettings @ObservedObject var updaterBridge: UpdaterBridge + @State private var initialLanguage: AppLanguage? + + private static let standardTimeouts = [10, 20, 30, 40, 50, 60, 90, 120, 180, 300, 600] + + /// Timeout options including the current value if it's non-standard + private var queryTimeoutOptions: [Int] { + let current = settings.queryTimeoutSeconds + if current > 0, !Self.standardTimeouts.contains(current) { + return (Self.standardTimeouts + [current]).sorted() + } + return Self.standardTimeouts + } var body: some View { Form { + Picker("Language:", selection: $settings.language) { + ForEach(AppLanguage.allCases) { lang in + Text(lang.displayName).tag(lang) + } + } + + if let initial = initialLanguage, settings.language != initial { + Text("Restart TablePro for the language change to take full effect.") + .font(.caption) + .foregroundStyle(.secondary) + } + Picker("When TablePro starts:", selection: $settings.startupBehavior) { ForEach(StartupBehavior.allCases) { behavior in Text(behavior.displayName).tag(behavior) @@ -23,7 +47,7 @@ struct GeneralSettingsView: View { Section("Query Execution") { Picker("Query timeout:", selection: $settings.queryTimeoutSeconds) { Text("No limit").tag(0) - ForEach([10, 20, 30, 40, 50, 60, 90, 120, 180, 300, 600], id: \.self) { seconds in + ForEach(queryTimeoutOptions, id: \.self) { seconds in Text("\(seconds) seconds").tag(seconds) } } @@ -45,6 +69,9 @@ struct GeneralSettingsView: View { .formStyle(.grouped) .scrollContentBackground(.hidden) .onAppear { + if initialLanguage == nil { + initialLanguage = settings.language + } updaterBridge.updater.automaticallyChecksForUpdates = settings.automaticallyCheckForUpdates } } diff --git a/TablePro/Views/Settings/HistorySettingsView.swift b/TablePro/Views/Settings/HistorySettingsView.swift index 368365e54..e3f002162 100644 --- a/TablePro/Views/Settings/HistorySettingsView.swift +++ b/TablePro/Views/Settings/HistorySettingsView.swift @@ -41,10 +41,10 @@ struct HistorySettingsView: View { Button("Clear History...") { Task { @MainActor in let confirmed = await AlertHelper.confirmDestructive( - title: "Clear All History?", - message: "This will permanently delete all query history entries. This action cannot be undone.", - confirmButton: "Clear", - cancelButton: "Cancel" + title: String(localized: "Clear All History?"), + message: String(localized: "This will permanently delete all query history entries. This action cannot be undone."), + confirmButton: String(localized: "Clear"), + cancelButton: String(localized: "Cancel") ) if confirmed { diff --git a/TablePro/Views/Settings/LicenseSettingsView.swift b/TablePro/Views/Settings/LicenseSettingsView.swift index 1697ad897..2de6a9d4c 100644 --- a/TablePro/Views/Settings/LicenseSettingsView.swift +++ b/TablePro/Views/Settings/LicenseSettingsView.swift @@ -46,10 +46,10 @@ struct LicenseSettingsView: View { Button("Deactivate...") { Task { @MainActor in let confirmed = await AlertHelper.confirmDestructive( - title: "Deactivate License?", - message: "This will remove the license from this machine. You can reactivate later.", - confirmButton: "Deactivate", - cancelButton: "Cancel" + title: String(localized: "Deactivate License?"), + message: String(localized: "This will remove the license from this machine. You can reactivate later."), + confirmButton: String(localized: "Deactivate"), + cancelButton: String(localized: "Cancel") ) if confirmed { @@ -107,7 +107,7 @@ struct LicenseSettingsView: View { licenseKeyInput = "" } catch { AlertHelper.showErrorSheet( - title: "Activation Failed", + title: String(localized: "Activation Failed"), message: error.localizedDescription, window: NSApp.keyWindow ) @@ -119,7 +119,7 @@ struct LicenseSettingsView: View { try await licenseManager.deactivate() } catch { AlertHelper.showErrorSheet( - title: "Deactivation Failed", + title: String(localized: "Deactivation Failed"), message: error.localizedDescription, window: NSApp.keyWindow ) diff --git a/TablePro/Views/Sidebar/SidebarView.swift b/TablePro/Views/Sidebar/SidebarView.swift index 9fd415e30..4c660d2d0 100644 --- a/TablePro/Views/Sidebar/SidebarView.swift +++ b/TablePro/Views/Sidebar/SidebarView.swift @@ -115,7 +115,7 @@ struct SidebarView: View { let tables = pendingOperationTables if let firstTable = tables.first { let tableName = tables.count > 1 - ? "\(tables.count) tables" + ? String(localized: "\(tables.count) tables") : firstTable TableOperationDialog( isPresented: $showOperationDialog, @@ -362,7 +362,7 @@ struct SidebarView: View { .disabled(!hasSelection || isReadOnly) } - Button(isView ? "Drop View" : "Delete", role: .destructive) { + Button(isView ? String(localized: "Drop View") : String(localized: "Delete"), role: .destructive) { if selectedTables.isEmpty, let table = clickedTable { selectedTables.insert(table) } diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index d174b6c6e..25d373729 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -522,7 +522,7 @@ struct TableStructureView: View { } catch { isReloadingAfterSave = false // Clear flag on error AlertHelper.showErrorSheet( - title: "Error Applying Changes", + title: String(localized: "Error Applying Changes"), message: error.localizedDescription, window: nil ) @@ -764,10 +764,10 @@ struct TableStructureView: View { // Show confirmation dialog Task { @MainActor in let confirmed = await AlertHelper.confirmDestructive( - title: "Discard Changes?", - message: "You have unsaved changes to the table structure. Refreshing will discard these changes.", - confirmButton: "Discard", - cancelButton: "Cancel" + title: String(localized: "Discard Changes?"), + message: String(localized: "You have unsaved changes to the table structure. Refreshing will discard these changes."), + confirmButton: String(localized: "Discard"), + cancelButton: String(localized: "Cancel") ) if confirmed { diff --git a/TablePro/Views/Toolbar/ExecutionIndicatorView.swift b/TablePro/Views/Toolbar/ExecutionIndicatorView.swift index df7414cbb..38fda52d4 100644 --- a/TablePro/Views/Toolbar/ExecutionIndicatorView.swift +++ b/TablePro/Views/Toolbar/ExecutionIndicatorView.swift @@ -41,15 +41,17 @@ struct ExecutionIndicatorView: View { /// Format duration for display private func formattedDuration(_ duration: TimeInterval) -> String { if duration < 0.001 { - return "<1ms" + return String(localized: "<1ms") } else if duration < 1.0 { - return String(format: "%.0fms", duration * 1_000) + let ms = String(format: "%.0f", duration * 1_000) + return String(localized: "\(ms)ms") } else if duration < 60.0 { - return String(format: "%.2fs", duration) + let secs = String(format: "%.2f", duration) + return String(localized: "\(secs)s") } else { let minutes = Int(duration) / 60 let seconds = Int(duration) % 60 - return "\(minutes)m \(seconds)s" + return String(localized: "\(minutes)m \(seconds)s") } } } diff --git a/TablePro/Views/Toolbar/ToolbarItemFactory.swift b/TablePro/Views/Toolbar/ToolbarItemFactory.swift index adf6ac7ee..f036d4bba 100644 --- a/TablePro/Views/Toolbar/ToolbarItemFactory.swift +++ b/TablePro/Views/Toolbar/ToolbarItemFactory.swift @@ -120,7 +120,7 @@ final class DefaultToolbarItemFactory: ToolbarItemFactory { item.toolTip = ToolbarItemIdentifier.newQueryTab.toolTip let button = NSButton( - title: "SQL", + title: String(localized: "SQL"), target: ToolbarActionProxy.shared, action: #selector(ToolbarActionProxy.newQueryTabAction) ) diff --git a/TablePro/Views/Toolbar/ToolbarItemIdentifier.swift b/TablePro/Views/Toolbar/ToolbarItemIdentifier.swift index f8adbb37f..622224299 100644 --- a/TablePro/Views/Toolbar/ToolbarItemIdentifier.swift +++ b/TablePro/Views/Toolbar/ToolbarItemIdentifier.swift @@ -59,32 +59,32 @@ enum ToolbarItemIdentifier: String, CaseIterable { /// Note: connectionStatus label is set dynamically based on connection name var label: String { switch self { - case .connectionSwitcher: return "Connection" - case .databaseSwitcher: return "Database" - case .newQueryTab: return "SQL" - case .refresh: return "Refresh" + case .connectionSwitcher: return String(localized: "Connection") + case .databaseSwitcher: return String(localized: "Database") + case .newQueryTab: return String(localized: "SQL") + case .refresh: return String(localized: "Refresh") case .connectionStatus: return "" // Set dynamically in ToolbarItemFactory - case .filterToggle: return "Filters" - case .historyToggle: return "History" - case .export: return "Export" - case .import: return "Import" - case .inspector: return "Inspector" + case .filterToggle: return String(localized: "Filters") + case .historyToggle: return String(localized: "History") + case .export: return String(localized: "Export") + case .import: return String(localized: "Import") + case .inspector: return String(localized: "Inspector") } } /// Label shown in customization palette var paletteLabel: String { switch self { - case .connectionSwitcher: return "Connection Switcher" - case .databaseSwitcher: return "Database Switcher" - case .newQueryTab: return "New Query Tab" - case .refresh: return "Refresh" - case .connectionStatus: return "Connection Status" - case .filterToggle: return "Toggle Filters" - case .historyToggle: return "Toggle History" - case .export: return "Export Data" - case .import: return "Import Data" - case .inspector: return "Toggle Inspector" + case .connectionSwitcher: return String(localized: "Connection Switcher") + case .databaseSwitcher: return String(localized: "Database Switcher") + case .newQueryTab: return String(localized: "New Query Tab") + case .refresh: return String(localized: "Refresh") + case .connectionStatus: return String(localized: "Connection Status") + case .filterToggle: return String(localized: "Toggle Filters") + case .historyToggle: return String(localized: "Toggle History") + case .export: return String(localized: "Export Data") + case .import: return String(localized: "Import Data") + case .inspector: return String(localized: "Toggle Inspector") } } @@ -92,25 +92,25 @@ enum ToolbarItemIdentifier: String, CaseIterable { var toolTip: String { switch self { case .connectionSwitcher: - return "Switch Connection" + return String(localized: "Switch Connection") case .databaseSwitcher: - return "Switch Database (⌘K)" + return String(localized: "Switch Database (⌘K)") case .newQueryTab: - return "New Query Tab (⌘T)" + return String(localized: "New Query Tab (⌘T)") case .refresh: - return "Refresh (⌘R)" + return String(localized: "Refresh (⌘R)") case .connectionStatus: - return "Connection Status" + return String(localized: "Connection Status") case .filterToggle: - return "Toggle Filters (⌘F)" + return String(localized: "Toggle Filters (⌘F)") case .historyToggle: - return "Toggle Query History (⌘⇧H)" + return String(localized: "Toggle Query History (⌘⇧H)") case .export: - return "Export Data (⌘⇧E)" + return String(localized: "Export Data (⌘⇧E)") case .import: - return "Import Data (⌘⇧I)" + return String(localized: "Import Data (⌘⇧I)") case .inspector: - return "Toggle Inspector (⌘⌥B)" + return String(localized: "Toggle Inspector (⌘⌥B)") } } diff --git a/TablePro/Views/WelcomeWindowView.swift b/TablePro/Views/WelcomeWindowView.swift index 45814bf1f..d14bc5cac 100644 --- a/TablePro/Views/WelcomeWindowView.swift +++ b/TablePro/Views/WelcomeWindowView.swift @@ -287,7 +287,7 @@ struct WelcomeWindowView: View { // Show error to user and re-open welcome window await MainActor.run { AlertHelper.showErrorSheet( - title: "Connection Failed", + title: String(localized: "Connection Failed"), message: error.localizedDescription, window: nil )