Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
24 changes: 15 additions & 9 deletions TRACKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
hasScannedForEncodings = 0;
knownRegions = (
en,
vi,
Base,
);
mainGroup = 5A1091BE2EF17EDC0055EA7C;
Expand Down
4 changes: 2 additions & 2 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""
)
Expand All @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions TablePro/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions TablePro/Core/SSH/SSHTunnelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions TablePro/Core/Services/SQLFormatterTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions TablePro/Core/Storage/AppSettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ final class AppSettingsManager: ObservableObject {

@Published var general: GeneralSettings {
didSet {
general.language.apply()
storage.saveGeneral(general)
notifyChange(domain: "general", notification: .generalSettingsDidChange)
}
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions TablePro/Core/Utilities/AlertHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions TablePro/Core/Validation/SettingsValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Expand Down
Loading