diff --git a/CLAUDE.md b/CLAUDE.md index 84c1542..a7b5aa6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,8 +14,6 @@ For architecture and decision rationale, see [ARCHITECTURE.md](ARCHITECTURE.md). - **Frameworks**: IOKit.pwr_mgt (power assertions), ServiceManagement (login items), Foundation (UserDefaults) - **Target**: macOS 15.6 (Sequoia), Apple Silicon + Intel - **Dependencies**: None. Zero third-party packages. -- **Build**: `xcodebuild build -project app/Vigil.xcodeproj -scheme Vigil -destination 'platform=macOS'` -- **Test**: `xcodebuild test -project app/Vigil.xcodeproj -scheme Vigil -destination 'platform=macOS'` ## Project Structure @@ -26,7 +24,7 @@ vigil/ ├── PRD.md # Product requirements ├── app/ # macOS app (Xcode project) │ ├── Vigil.xcodeproj/ -│ ├── Vigil/ # Source + PrivacyInfo.xcprivacy +│ ├── Vigil/ # Source + PrivacyInfo.xcprivacy + Localizable.xcstrings │ └── VigilTests/ ├── appstore/ # App Store screenshots (2880×1800) └── website/ # Static promo site (Vercel): vigil-for-mac.vercel.app @@ -43,6 +41,7 @@ New `.swift` files in `app/Vigil/` are auto-included in the build (PBXFileSystem - `// MARK: -` comments for view section organization - `import IOKit.pwr_mgt` (submodule import, not `import IOKit`) - Computed properties for view decomposition (`heroSection`, `modeSection`, etc.) +- `LocalizedStringResource` for localizable strings in enums/models (not plain `String`) — enables auto-extraction into String Catalogs ## Testing @@ -53,6 +52,37 @@ New `.swift` files in `app/Vigil/` are auto-included in the build (PBXFileSystem - **Integration-style**: Tests create real IOPMAssertions and verify via `findAssertions(forPid:)`, a helper that queries the kernel with `IOPMCopyAssertionsByProcess` - **Entry point**: `AppLauncher` (the actual `@main`) detects test runs via `NSClassFromString("XCTestCase")` and substitutes a lightweight `TestApp`. Do not add `@main` to `VigilApp` directly. +## Localization + +- **Catalog**: Single `Localizable.xcstrings` (String Catalog) — all languages in one file +- **Languages**: English (source), German (de), Spanish (es), Hindi (hi), Ukrainian (uk), Chinese Simplified (zh-Hans) +- **SwiftUI views**: `Text("...")` and `Label("...")` strings are auto-extracted on build — no manual registration +- **Enum/model strings**: Return `LocalizedStringResource` (not `String`) so Xcode auto-extracts them too +- **Not localized**: `SleepMode.assertionReason` — intentionally English (appears in `pmset` output and Activity Monitor) +- **Adding a new string**: Just use `Text("New string")` or return `LocalizedStringResource`. Build the project — the key appears in `Localizable.xcstrings` marked "New". Add translations there. +- **Testing a language**: In Xcode: Edit Scheme → Run → Options → App Language. See CLI Commands below for command-line testing. + +## CLI Commands + +```bash +# Build +xcodebuild build -project app/Vigil.xcodeproj -scheme Vigil -destination 'platform=macOS' + +# Test +xcodebuild test -project app/Vigil.xcodeproj -scheme Vigil -destination 'platform=macOS' + +# Build and run +xcodebuild build -project app/Vigil.xcodeproj -scheme Vigil -destination 'platform=macOS' -quiet && \ +open "$(xcodebuild -project app/Vigil.xcodeproj -scheme Vigil -showBuildSettings 2>/dev/null | grep '^ *BUILT_PRODUCTS_DIR = ' | sed 's/.*= //')/Vigil.app" + +# Build and run with a specific language (replace 'uk' with: de, es, hi, zh-Hans) +xcodebuild build -project app/Vigil.xcodeproj -scheme Vigil -destination 'platform=macOS' -quiet && \ +open "$(xcodebuild -project app/Vigil.xcodeproj -scheme Vigil -showBuildSettings 2>/dev/null | grep '^ *BUILT_PRODUCTS_DIR = ' | sed 's/.*= //')/Vigil.app" --args -AppleLanguages '(uk)' + +# Verify sleep assertion is active +pmset -g assertions | grep Vigil +``` + ## Distribution - **App Store name**: "Vigil - Stay Awake" (managed in App Store Connect, separate from PRODUCT_NAME) diff --git a/app/Vigil.xcodeproj/project.pbxproj b/app/Vigil.xcodeproj/project.pbxproj index 99ef67e..a5e95d1 100644 --- a/app/Vigil.xcodeproj/project.pbxproj +++ b/app/Vigil.xcodeproj/project.pbxproj @@ -152,14 +152,10 @@ en, Base, de, - fr, es, - "es-US", - uk, - "en-GB", - "en-IN", hi, - ar, + uk, + "zh-Hans", ); mainGroup = 8C983B4B2F634369004A6B1A; minimizedProjectReferenceProxies = 1; diff --git a/app/Vigil/Localizable.xcstrings b/app/Vigil/Localizable.xcstrings new file mode 100644 index 0000000..9921d65 --- /dev/null +++ b/app/Vigil/Localizable.xcstrings @@ -0,0 +1,380 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Display & System" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildschirm & System" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pantalla y sistema" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "डिस्प्ले और सिस्टम" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екран і система" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "显示器和系统" + } + } + } + }, + "Launch at Login" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bei Anmeldung starten" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir al iniciar sesión" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "लॉगिन पर लॉन्च करें" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запускати при вході" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "登录时启动" + } + } + } + }, + "Mode" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modus" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मोड" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Режим" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "模式" + } + } + } + }, + "Quit Vigil" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vigil beenden" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir de Vigil" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vigil बंद करें" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Завершити Vigil" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "退出 Vigil" + } + } + } + }, + "Remember Last State" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Letzten Status merken" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recordar último estado" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पिछली स्थिति याद रखें" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запам'ятати останній стан" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "记住上次状态" + } + } + } + }, + "Screen and system stay awake" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildschirm und System bleiben wach" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La pantalla y el sistema permanecen activos" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्क्रीन और सिस्टम जागते रहेंगे" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екран і система не засинатимуть" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "屏幕和系统保持唤醒" + } + } + } + }, + "Screen may sleep, system stays running" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildschirm kann schlafen, System läuft weiter" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La pantalla puede apagarse, el sistema sigue activo" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्क्रीन स्लीप हो सकती है, सिस्टम चलता रहेगा" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Екран може заснути, система працюватиме" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "屏幕可能休眠,系统保持运行" + } + } + } + }, + "Sleep prevention is off" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schlafsperre ist deaktiviert" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La prevención de reposo está desactivada" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्लीप प्रिवेंशन बंद है" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запобігання сну вимкнено" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "防休眠已关闭" + } + } + } + }, + "Sleep prevention is on" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schlafsperre ist aktiviert" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "La prevención de reposo está activada" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "स्लीप प्रिवेंशन चालू है" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Запобігання сну увімкнено" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "防休眠已开启" + } + } + } + }, + "Stay Awake" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wach bleiben" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantener activo" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "जागते रहें" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не засинати" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持唤醒" + } + } + } + }, + "System Only" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nur System" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Solo sistema" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "केवल सिस्टम" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Лише система" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仅系统" + } + } + } + } + }, + "version" : "1.0" +} diff --git a/app/Vigil/MenuBarView.swift b/app/Vigil/MenuBarView.swift index 4778ed3..750bf0a 100644 --- a/app/Vigil/MenuBarView.swift +++ b/app/Vigil/MenuBarView.swift @@ -85,7 +85,7 @@ struct MenuBarView: View { .pickerStyle(.segmented) .labelsHidden() - Text(sleepManager.sleepMode.description) + Text(sleepManager.sleepMode.modeDescription) .font(.caption) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .center) diff --git a/app/Vigil/SleepManager.swift b/app/Vigil/SleepManager.swift index 73e2d06..27ea310 100644 --- a/app/Vigil/SleepManager.swift +++ b/app/Vigil/SleepManager.swift @@ -24,14 +24,14 @@ enum SleepMode: String, CaseIterable { } } - var label: String { + var label: LocalizedStringResource { switch self { case .displayAndSystem: "Display & System" case .systemOnly: "System Only" } } - var description: String { + var modeDescription: LocalizedStringResource { switch self { case .displayAndSystem: "Screen and system stay awake" case .systemOnly: "Screen may sleep, system stays running"