Ariver
2026-06-28 71a520840eb3debe46173c1f73bc85bbea0b1aa6
Implement Pro custom hotkeys for 8.2.0
37 files modified
8 files added
1728 ■■■■■ changed files
src/Apptag/ApptagApp.swift 114 ●●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/LauncherHotkeySettings.swift 234 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar-Najdi.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/cs.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/da.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/de.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/en.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/es.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/fr.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/id.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/it.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ja.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ko.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ms.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nb.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nl.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nn.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/no.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/pl.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/pt-BR.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ro.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ru.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/sr-Cyrl.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/sv.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/th.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/tr.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/uk.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/vi.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hans.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hant.json 18 ●●●●● patch | view | raw | blame | history
src/Apptag/PreferencesView.swift 178 ●●●●● patch | view | raw | blame | history
src/Apptag/ProAccessViews.swift 2 ●●●●● patch | view | raw | blame | history
src/Apptag/ProEntitlement.swift 12 ●●●●● patch | view | raw | blame | history
src/Apptag/QuickSearch.swift 5 ●●●●● patch | view | raw | blame | history
src/CHANGELOG.md 12 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-28-pro-custom-hotkeys-prd.md 329 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-28-pro-custom-hotkeys-todo.md 55 ●●●●● patch | view | raw | blame | history
src/Release/AppStore-8.2.0-20260628.2308/Archive/SHA256SUMS.txt 1 ●●●● patch | view | raw | blame | history
src/Release/AppStore-8.2.0-20260628.2308/Archive/TagLauncher-8.2.0-build20260628.2308.dmg patch | view | raw | blame | history
src/Release/AppStore-8.2.0-20260628.2308/QA/QA-Report.md 45 ●●●●● patch | view | raw | blame | history
src/Release/AppStore-8.2.0-20260628.2308/RELEASE_MANIFEST.md 27 ●●●●● patch | view | raw | blame | history
src/Scripts/pro_custom_hotkeys_qa.sh 179 ●●●●● patch | view | raw | blame | history
src/Scripts/pro_feature_gate_qa.sh 9 ●●●●● patch | view | raw | blame | history
src/Apptag/ApptagApp.swift
@@ -28,6 +28,11 @@
// MARK: - App Delegate (menubar + overlay window + hotkey)
private enum HotkeyRegistrationAttempt {
    case success(EventHotKeyRef)
    case failure(OSStatus)
}
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
    private static let showDockIconKey = "showDockIcon"
    private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName
@@ -220,6 +225,7 @@
        observeApplicationMenuChanges()
        observeChromeSettings()
        observeLanguageChanges()
        observeProEntitlementChanges()
        setupLaunchAtLogin()
        suppressReopenUntil = Date().addingTimeInterval(1.0)
        didFinishLaunching = true
@@ -889,7 +895,7 @@
        if LauncherHotkeyRegistrationStore.state(for: .main) == .failed {
            return tr("menu.showShortcutUnavailable")
        }
        return "\(tr("menu.showAppList"))  \(LauncherHotkey.main.displayString)"
        return "\(tr("menu.showAppList"))  \(LauncherHotkeySettings.effectiveHotkey(for: .main).displayString)"
    }
    private func observeApplicationMenuChanges() {
@@ -1268,13 +1274,91 @@
    private func registerConfiguredHotkeys() {
        installHotkeyEventHandlerIfNeeded()
        registerFixedHotkey(.main)
        registerFixedHotkey(.quickSearch)
        registerEffectiveHotkey(.main)
        registerEffectiveHotkey(.quickSearch)
    }
    private func registerFixedHotkey(_ kind: LauncherHotkeyKind) {
    private func registerEffectiveHotkey(_ kind: LauncherHotkeyKind) {
        unregisterHotkey(for: kind)
        let hotkey = kind.hotkey
        let hotkey = LauncherHotkeySettings.effectiveHotkey(for: kind)
        registerAndStoreHotkey(hotkey, for: kind, markFailure: true)
    }
    @discardableResult
    func applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult {
        guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
            return .locked
        }
        if let error = LauncherHotkeySettings.validationError(for: hotkey, kind: kind) {
            return .invalid(error)
        }
        return activateHotkey(hotkey, for: kind, persistCustom: true)
    }
    @discardableResult
    func restoreDefaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult {
        guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
            return .locked
        }
        return activateHotkey(LauncherHotkeySettings.defaultHotkey(for: kind), for: kind, persistCustom: false)
    }
    private func activateHotkey(
        _ hotkey: LauncherHotkey,
        for kind: LauncherHotkeyKind,
        persistCustom: Bool
    ) -> LauncherHotkeyCustomizationResult {
        installHotkeyEventHandlerIfNeeded()
        if LauncherHotkeySettings.effectiveHotkey(for: kind) == hotkey,
           hotkeyRef(for: kind) != nil {
            if persistCustom {
                LauncherHotkeySettings.save(hotkey, for: kind)
                LauncherHotkeyRegistrationStore.setActive(for: kind)
                return .saved
            }
            LauncherHotkeySettings.clearCustomHotkey(for: kind)
            LauncherHotkeyRegistrationStore.setActive(for: kind)
            return .restored
        }
        switch tryRegisterHotkey(hotkey, for: kind, markFailure: false) {
        case .success(let newRef):
            if let oldRef = hotkeyRef(for: kind) {
                UnregisterEventHotKey(oldRef)
            }
            setHotkeyRef(newRef, for: kind)
            if persistCustom {
                LauncherHotkeySettings.save(hotkey, for: kind)
                LauncherHotkeyRegistrationStore.setActive(for: kind)
                return .saved
            }
            LauncherHotkeySettings.clearCustomHotkey(for: kind)
            LauncherHotkeyRegistrationStore.setActive(for: kind)
            return .restored
        case .failure(let status):
            return .registrationFailed(status)
        }
    }
    private func registerAndStoreHotkey(
        _ hotkey: LauncherHotkey,
        for kind: LauncherHotkeyKind,
        markFailure: Bool
    ) {
        switch tryRegisterHotkey(hotkey, for: kind, markFailure: markFailure) {
        case .success(let newRef):
            setHotkeyRef(newRef, for: kind)
        case .failure:
            setHotkeyRef(nil, for: kind)
        }
    }
    private func tryRegisterHotkey(
        _ hotkey: LauncherHotkey,
        for kind: LauncherHotkeyKind,
        markFailure: Bool
    ) -> HotkeyRegistrationAttempt {
        var hotkeyID = EventHotKeyID()
        hotkeyID.signature = OSType(0x41505447) // 'APTG'
@@ -1291,12 +1375,16 @@
        )
        if status == noErr, let newRef {
            setHotkeyRef(newRef, for: kind)
            if markFailure {
            LauncherHotkeyRegistrationStore.setActive(for: kind)
            }
            return .success(newRef)
        } else {
            setHotkeyRef(nil, for: kind)
            if markFailure {
            LauncherHotkeyRegistrationStore.setFailed(status, for: kind)
            }
            print("[TagLauncher] Fixed hotkey registration failed for \(kind.rawValue): \(status)")
            return .failure(status)
        }
    }
@@ -1311,6 +1399,18 @@
        }
    }
    private func observeProEntitlementChanges() {
        NotificationCenter.default.addObserver(
            forName: .tagLauncherProEntitlementChanged,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.registerConfiguredHotkeys()
            self?.setupMenuBar()
            self?.configureApplicationMenuWhenAvailable(retries: 2)
        }
    }
    private func installHotkeyEventHandlerIfNeeded() {
        guard !hotkeyEventHandlerInstalled else { return }
        hotkeyEventHandlerInstalled = true
src/Apptag/Info.plist
@@ -19,7 +19,7 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>8.1.7</string>
    <string>8.2.0</string>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
@@ -32,7 +32,7 @@
        </dict>
    </array>
    <key>CFBundleVersion</key>
    <string>20260628.2035</string>
    <string>20260628.2308</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/Apptag/LauncherHotkeySettings.swift
New file
@@ -0,0 +1,234 @@
import AppKit
import Carbon
import Foundation
enum LauncherHotkeyValidationError: Equatable {
    case modifierRequired
    case unsupportedKey
    case reservedBySystem
    case duplicate
    var messageKey: String {
        switch self {
        case .modifierRequired:
            return "hotkeys.error.modifierRequired"
        case .unsupportedKey:
            return "hotkeys.error.unsupportedKey"
        case .reservedBySystem:
            return "hotkeys.error.reserved"
        case .duplicate:
            return "hotkeys.error.duplicate"
        }
    }
}
enum LauncherHotkeyCustomizationResult: Equatable {
    case saved
    case restored
    case locked
    case invalid(LauncherHotkeyValidationError)
    case registrationFailed(OSStatus)
    var messageKey: String {
        switch self {
        case .saved:
            return "hotkeys.saved"
        case .restored:
            return "hotkeys.restored"
        case .locked:
            return "hotkeys.proRequired"
        case .invalid(let error):
            return error.messageKey
        case .registrationFailed:
            return "hotkeys.error.registrationFailed"
        }
    }
}
enum LauncherHotkeySettings {
    private static let namespace = "customHotkeys.v1"
    private static let allowedModifierMask = UInt32(
        controlKey | optionKey | shiftKey | cmdKey | kEventKeyModifierFnMask
    )
    static func defaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey {
        kind.hotkey
    }
    static func effectiveHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey {
        guard ProEntitlementPolicy.isUnlocked(.customHotkeys),
              let saved = savedHotkey(for: kind),
              structuralValidationError(for: saved) == nil,
              !duplicatesOtherEffectiveHotkey(saved, kind: kind)
        else {
            return defaultHotkey(for: kind)
        }
        return normalized(saved)
    }
    static func hasCustomHotkey(for kind: LauncherHotkeyKind) -> Bool {
        savedHotkey(for: kind) != nil
    }
    static func savedHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey? {
        let defaults = UserDefaults.standard
        guard defaults.object(forKey: keyCodeKey(for: kind)) != nil,
              defaults.object(forKey: modifiersKey(for: kind)) != nil
        else {
            return nil
        }
        let keyCode = defaults.integer(forKey: keyCodeKey(for: kind))
        let modifiers = defaults.integer(forKey: modifiersKey(for: kind))
        guard keyCode >= 0, modifiers >= 0 else { return nil }
        return LauncherHotkey(
            keyCode: UInt32(keyCode),
            modifiers: UInt32(modifiers) & allowedModifierMask
        )
    }
    static func save(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) {
        let defaults = UserDefaults.standard
        defaults.set(Int(hotkey.keyCode), forKey: keyCodeKey(for: kind))
        defaults.set(Int(hotkey.modifiers & allowedModifierMask), forKey: modifiersKey(for: kind))
        NotificationCenter.default.post(name: .tagLauncherHotkeyRegistrationChanged, object: nil)
    }
    static func clearCustomHotkey(for kind: LauncherHotkeyKind) {
        let defaults = UserDefaults.standard
        defaults.removeObject(forKey: keyCodeKey(for: kind))
        defaults.removeObject(forKey: modifiersKey(for: kind))
        NotificationCenter.default.post(name: .tagLauncherHotkeyRegistrationChanged, object: nil)
    }
    static func candidate(from event: NSEvent) -> LauncherHotkey {
        LauncherHotkey(
            keyCode: UInt32(event.keyCode),
            modifiers: carbonModifiers(from: event.modifierFlags)
        )
    }
    static func validationError(for hotkey: LauncherHotkey, kind: LauncherHotkeyKind) -> LauncherHotkeyValidationError? {
        let normalizedHotkey = normalized(hotkey)
        if let structuralError = structuralValidationError(for: normalizedHotkey) {
            return structuralError
        }
        if duplicatesOtherEffectiveHotkey(normalizedHotkey, kind: kind) {
            return .duplicate
        }
        return nil
    }
    private static func normalized(_ hotkey: LauncherHotkey) -> LauncherHotkey {
        LauncherHotkey(
            keyCode: hotkey.keyCode,
            modifiers: hotkey.modifiers & allowedModifierMask
        )
    }
    private static func structuralValidationError(for hotkey: LauncherHotkey) -> LauncherHotkeyValidationError? {
        guard isSupportedKey(hotkey.keyCode) else {
            return .unsupportedKey
        }
        guard hasSupportedModifierCombination(hotkey) else {
            return .modifierRequired
        }
        guard !isReservedBySystem(hotkey) else {
            return .reservedBySystem
        }
        return nil
    }
    private static func duplicatesOtherEffectiveHotkey(_ hotkey: LauncherHotkey, kind: LauncherHotkeyKind) -> Bool {
        let otherKind: LauncherHotkeyKind = kind == .main ? .quickSearch : .main
        return hotkey == effectiveHotkeyIgnoringDuplicateCheck(for: otherKind)
    }
    private static func effectiveHotkeyIgnoringDuplicateCheck(for kind: LauncherHotkeyKind) -> LauncherHotkey {
        guard ProEntitlementPolicy.isUnlocked(.customHotkeys),
              let saved = savedHotkey(for: kind),
              structuralValidationError(for: saved) == nil
        else {
            return defaultHotkey(for: kind)
        }
        return normalized(saved)
    }
    private static func keyCodeKey(for kind: LauncherHotkeyKind) -> String {
        "\(namespace).\(kind.rawValue).keyCode"
    }
    private static func modifiersKey(for kind: LauncherHotkeyKind) -> String {
        "\(namespace).\(kind.rawValue).modifiers"
    }
    private static func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 {
        let normalized = flags.intersection(.deviceIndependentFlagsMask)
        var modifiers: UInt32 = 0
        if normalized.contains(.control) {
            modifiers |= UInt32(controlKey)
        }
        if normalized.contains(.option) {
            modifiers |= UInt32(optionKey)
        }
        if normalized.contains(.shift) {
            modifiers |= UInt32(shiftKey)
        }
        if normalized.contains(.command) {
            modifiers |= UInt32(cmdKey)
        }
        if normalized.contains(.function) {
            modifiers |= UInt32(kEventKeyModifierFnMask)
        }
        return modifiers & allowedModifierMask
    }
    private static func hasSupportedModifierCombination(_ hotkey: LauncherHotkey) -> Bool {
        let modifiers = hotkey.modifiers & allowedModifierMask
        guard modifiers != 0 else { return false }
        if modifiers == UInt32(shiftKey) || modifiers == UInt32(optionKey) {
            return false
        }
        if modifiers == UInt32(kEventKeyModifierFnMask) {
            return hotkey.keyCode == UInt32(kVK_Space)
        }
        return true
    }
    private static func isSupportedKey(_ keyCode: UInt32) -> Bool {
        switch Int(keyCode) {
        case kVK_Space, kVK_Return, kVK_Tab, kVK_Delete:
            return true
        case kVK_LeftArrow, kVK_RightArrow, kVK_UpArrow, kVK_DownArrow:
            return true
        case kVK_F1...kVK_F12:
            return true
        default:
            return LauncherHotkey.printableKeyCodes.contains(Int(keyCode))
        }
    }
    private static func isReservedBySystem(_ hotkey: LauncherHotkey) -> Bool {
        let key = Int(hotkey.keyCode)
        let modifiers = hotkey.modifiers & allowedModifierMask
        let hasCommand = modifiers & UInt32(cmdKey) != 0
        let hasOption = modifiers & UInt32(optionKey) != 0
        let hasControl = modifiers & UInt32(controlKey) != 0
        if key == kVK_Space, hasCommand {
            return true
        }
        if key == kVK_Tab, hasCommand {
            return true
        }
        if key == kVK_Escape, hasCommand && hasOption {
            return true
        }
        if [kVK_ANSI_Q, kVK_ANSI_W, kVK_ANSI_H, kVK_ANSI_M].contains(key), hasCommand {
            return true
        }
        if [kVK_LeftArrow, kVK_RightArrow, kVK_UpArrow, kVK_DownArrow].contains(key), hasControl {
            return true
        }
        return false
    }
}
src/Apptag/Localization/ar-Najdi.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "تم فتح جميع السمات على جهاز Mac هذا.",
  "theme.tuning.dark": "ضبط الملوّن الداكن",
  "theme.tuning.light": "ضبط الملوّن الفاتح",
  "theme.tuning.desc": "اضبط تدفق الألوان السائل. السمة تبقى على أسلوبها الفاتح أو الداكن الحالي. السمات تغيّر خلفية شبكة التطبيقات فقط. الحاويات تبقى بسطح زجاجي موحّد."
  "theme.tuning.desc": "اضبط تدفق الألوان السائل. السمة تبقى على أسلوبها الفاتح أو الداكن الحالي. السمات تغيّر خلفية شبكة التطبيقات فقط. الحاويات تبقى بسطح زجاجي موحّد.",
  "pro.feature.customHotkeys": "اختصارات مخصصة",
  "pro.feature.customHotkeys.benefit": "عيّن اختصارات عامة لقائمة التطبيقات و Quick Search.",
  "pro.prompt.customHotkeys": "الاختصارات المخصصة جزء من Pro. افتح Pro لتعيين اختصاراتك العامة.",
  "hotkeys.customize": "تخصيص",
  "hotkeys.restoreDefault": "استعادة الافتراضي",
  "hotkeys.recording": "اضغط الاختصار الجديد الآن. اضغط Esc للإلغاء.",
  "hotkeys.recordingInline": "تسجيل",
  "hotkeys.recordingCancelled": "تم إلغاء تسجيل الاختصار.",
  "hotkeys.saved": "تم حفظ الاختصار.",
  "hotkeys.restored": "تمت استعادة الاختصار الافتراضي.",
  "hotkeys.proRequired": "افتح Pro لتخصيص الاختصارات.",
  "hotkeys.error.modifierRequired": "استخدم Command أو Control أو مفتاحي تعديل على الأقل.",
  "hotkeys.error.unsupportedKey": "هذا المفتاح غير مدعوم.",
  "hotkeys.error.reserved": "هذا الاختصار محجوز بواسطة macOS أو التطبيق.",
  "hotkeys.error.duplicate": "هذا الاختصار مستخدم بالفعل لإجراء آخر في TagLauncher.",
  "hotkeys.error.registrationFailed": "تعذر تسجيل الاختصار. رمز الخطأ: %status%."
}
src/Apptag/Localization/ar.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "تم فتح جميع السمات على جهاز Mac هذا.",
  "theme.tuning.dark": "ضبط الملوّن الداكن",
  "theme.tuning.light": "ضبط الملوّن الفاتح",
  "theme.tuning.desc": "اضبط تدفق الألوان السائل. تحافظ السمة على أسلوبها الفاتح أو الداكن الحالي. تغيّر السمات خلفية شبكة التطبيقات فقط. تبقى الحاويات بسطح زجاجي موحّد."
  "theme.tuning.desc": "اضبط تدفق الألوان السائل. تحافظ السمة على أسلوبها الفاتح أو الداكن الحالي. تغيّر السمات خلفية شبكة التطبيقات فقط. تبقى الحاويات بسطح زجاجي موحّد.",
  "pro.feature.customHotkeys": "اختصارات مخصصة",
  "pro.feature.customHotkeys.benefit": "عيّن اختصارات عامة لقائمة التطبيقات و Quick Search.",
  "pro.prompt.customHotkeys": "الاختصارات المخصصة جزء من Pro. افتح Pro لتعيين اختصاراتك العامة.",
  "hotkeys.customize": "تخصيص",
  "hotkeys.restoreDefault": "استعادة الافتراضي",
  "hotkeys.recording": "اضغط الاختصار الجديد الآن. اضغط Esc للإلغاء.",
  "hotkeys.recordingInline": "تسجيل",
  "hotkeys.recordingCancelled": "تم إلغاء تسجيل الاختصار.",
  "hotkeys.saved": "تم حفظ الاختصار.",
  "hotkeys.restored": "تمت استعادة الاختصار الافتراضي.",
  "hotkeys.proRequired": "افتح Pro لتخصيص الاختصارات.",
  "hotkeys.error.modifierRequired": "استخدم Command أو Control أو مفتاحي تعديل على الأقل.",
  "hotkeys.error.unsupportedKey": "هذا المفتاح غير مدعوم.",
  "hotkeys.error.reserved": "هذا الاختصار محجوز بواسطة macOS أو التطبيق.",
  "hotkeys.error.duplicate": "هذا الاختصار مستخدم بالفعل لإجراء آخر في TagLauncher.",
  "hotkeys.error.registrationFailed": "تعذر تسجيل الاختصار. رمز الخطأ: %status%."
}
src/Apptag/Localization/cs.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Všechna témata jsou na tomto Macu odemčená.",
  "theme.tuning.dark": "Ladění tmavě barevného",
  "theme.tuning.light": "Ladění světle barevného",
  "theme.tuning.desc": "Upravte tekoucí barevný přechod. Motiv zachová svůj světlý nebo tmavý styl. Motivy mění jen pozadí mřížky aplikací. Kontejnery si ponechají jednotný skleněný povrch."
  "theme.tuning.desc": "Upravte tekoucí barevný přechod. Motiv zachová svůj světlý nebo tmavý styl. Motivy mění jen pozadí mřížky aplikací. Kontejnery si ponechají jednotný skleněný povrch.",
  "pro.feature.customHotkeys": "Vlastní zkratky",
  "pro.feature.customHotkeys.benefit": "Nastavte globální zkratky pro seznam aplikací a Quick Search.",
  "pro.prompt.customHotkeys": "Vlastní zkratky jsou součástí Pro. Odemkněte Pro a nastavte je.",
  "hotkeys.customize": "Přizpůsobit",
  "hotkeys.restoreDefault": "Obnovit výchozí",
  "hotkeys.recording": "Stiskněte novou zkratku. Esc zruší.",
  "hotkeys.recordingInline": "Nahrávání",
  "hotkeys.recordingCancelled": "Nahrávání zkratky zrušeno.",
  "hotkeys.saved": "Zkratka uložena.",
  "hotkeys.restored": "Výchozí zkratka obnovena.",
  "hotkeys.proRequired": "Odemkněte Pro pro přizpůsobení zkratek.",
  "hotkeys.error.modifierRequired": "Použijte alespoň Command, Control nebo dva modifikátory.",
  "hotkeys.error.unsupportedKey": "Tato klávesa není podporována.",
  "hotkeys.error.reserved": "Tato zkratka je vyhrazena macOS nebo aplikací.",
  "hotkeys.error.duplicate": "Tato zkratka už patří jiné akci TagLauncher.",
  "hotkeys.error.registrationFailed": "Zkratku se nepodařilo zaregistrovat. Kód chyby: %status%."
}
src/Apptag/Localization/da.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alle temaer er låst op på denne Mac.",
  "theme.tuning.dark": "Juster mørk farverig",
  "theme.tuning.light": "Juster lys farverig",
  "theme.tuning.desc": "Juster det flydende farveforløb. Temaet bevarer sin lyse eller mørke stil. Temaer ændrer kun baggrunden i appgitteret. Containere beholder en ensartet glasflade."
  "theme.tuning.desc": "Juster det flydende farveforløb. Temaet bevarer sin lyse eller mørke stil. Temaer ændrer kun baggrunden i appgitteret. Containere beholder en ensartet glasflade.",
  "pro.feature.customHotkeys": "Egne genveje",
  "pro.feature.customHotkeys.benefit": "Indstil globale genveje til applisten og Quick Search.",
  "pro.prompt.customHotkeys": "Egne genveje er en Pro-funktion. Lås Pro op for at indstille dem.",
  "hotkeys.customize": "Tilpas",
  "hotkeys.restoreDefault": "Gendan standard",
  "hotkeys.recording": "Tryk på den nye genvej nu. Tryk Esc for at annullere.",
  "hotkeys.recordingInline": "Optager",
  "hotkeys.recordingCancelled": "Optagelse af genvej annulleret.",
  "hotkeys.saved": "Genvej gemt.",
  "hotkeys.restored": "Standardgenvej gendannet.",
  "hotkeys.proRequired": "Lås Pro op for at tilpasse genveje.",
  "hotkeys.error.modifierRequired": "Brug mindst Command, Control eller to modifikatorer.",
  "hotkeys.error.unsupportedKey": "Denne tast understøttes ikke.",
  "hotkeys.error.reserved": "Denne genvej er reserveret af macOS eller appen.",
  "hotkeys.error.duplicate": "Denne genvej bruges allerede af en anden TagLauncher-handling.",
  "hotkeys.error.registrationFailed": "Genvejen kunne ikke registreres. Fejlkode: %status%."
}
src/Apptag/Localization/de.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alle Themes sind auf diesem Mac freigeschaltet.",
  "theme.tuning.dark": "Dunkel bunt abstimmen",
  "theme.tuning.light": "Hell bunt abstimmen",
  "theme.tuning.desc": "Passe den fließenden Farbeindruck an. Das helle oder dunkle Design bleibt erhalten. Designs ändern nur den Hintergrund des App-Rasters. Container behalten eine einheitliche Glasfläche."
  "theme.tuning.desc": "Passe den fließenden Farbeindruck an. Das helle oder dunkle Design bleibt erhalten. Designs ändern nur den Hintergrund des App-Rasters. Container behalten eine einheitliche Glasfläche.",
  "pro.feature.customHotkeys": "Eigene Kurzbefehle",
  "pro.feature.customHotkeys.benefit": "Lege eigene globale Kurzbefehle für App-Liste und Quick Search fest.",
  "pro.prompt.customHotkeys": "Eigene Kurzbefehle sind Teil von Pro. Schalte Pro frei, um globale Kurzbefehle anzupassen.",
  "hotkeys.customize": "Anpassen",
  "hotkeys.restoreDefault": "Standard wiederherstellen",
  "hotkeys.recording": "Drücke jetzt den neuen Kurzbefehl. Mit Esc abbrechen.",
  "hotkeys.recordingInline": "Aufnahme",
  "hotkeys.recordingCancelled": "Kurzbefehl-Aufnahme abgebrochen.",
  "hotkeys.saved": "Kurzbefehl gespeichert.",
  "hotkeys.restored": "Standardkurzbefehl wiederhergestellt.",
  "hotkeys.proRequired": "Schalte Pro frei, um Kurzbefehle anzupassen.",
  "hotkeys.error.modifierRequired": "Verwende mindestens Command, Control oder zwei Sondertasten.",
  "hotkeys.error.unsupportedKey": "Diese Taste wird nicht unterstützt.",
  "hotkeys.error.reserved": "Dieser Kurzbefehl ist von macOS oder der App reserviert.",
  "hotkeys.error.duplicate": "Dieser Kurzbefehl gehört bereits zu einer anderen TagLauncher-Aktion.",
  "hotkeys.error.registrationFailed": "Kurzbefehl konnte nicht registriert werden. Fehlercode: %status%."
}
src/Apptag/Localization/en.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "All themes are unlocked on this Mac.",
  "theme.tuning.dark": "Dark colorful tuning",
  "theme.tuning.light": "Light colorful tuning",
  "theme.tuning.desc": "Adjust the liquid color flow. The theme keeps its current light or dark style. Themes only change the App Grid background. Containers keep one consistent glass surface."
  "theme.tuning.desc": "Adjust the liquid color flow. The theme keeps its current light or dark style. Themes only change the App Grid background. Containers keep one consistent glass surface.",
  "pro.feature.customHotkeys": "Custom shortcuts",
  "pro.feature.customHotkeys.benefit": "Set your own global shortcuts for the app list and Quick Search.",
  "pro.prompt.customHotkeys": "Custom shortcuts are part of Pro. Unlock Pro to set your own global shortcuts.",
  "hotkeys.customize": "Customize",
  "hotkeys.restoreDefault": "Restore default",
  "hotkeys.recording": "Press the new shortcut now. Press Esc to cancel.",
  "hotkeys.recordingInline": "Recording",
  "hotkeys.recordingCancelled": "Shortcut recording cancelled.",
  "hotkeys.saved": "Shortcut saved.",
  "hotkeys.restored": "Default shortcut restored.",
  "hotkeys.proRequired": "Unlock Pro to customize shortcuts.",
  "hotkeys.error.modifierRequired": "Use at least Command, Control, or two modifiers.",
  "hotkeys.error.unsupportedKey": "This key is not supported.",
  "hotkeys.error.reserved": "This shortcut is reserved by macOS or the app.",
  "hotkeys.error.duplicate": "This shortcut already belongs to another TagLauncher action.",
  "hotkeys.error.registrationFailed": "Shortcut could not be registered. Error code: %status%."
}
src/Apptag/Localization/es.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Todos los temas están desbloqueados en esta Mac.",
  "theme.tuning.dark": "Ajuste colorido oscuro",
  "theme.tuning.light": "Ajuste colorido claro",
  "theme.tuning.desc": "Ajusta el flujo líquido de color. El tema conserva su estilo claro u oscuro. Los temas solo cambian el fondo de la cuadrícula de apps. Los contenedores mantienen una superficie de cristal uniforme."
  "theme.tuning.desc": "Ajusta el flujo líquido de color. El tema conserva su estilo claro u oscuro. Los temas solo cambian el fondo de la cuadrícula de apps. Los contenedores mantienen una superficie de cristal uniforme.",
  "pro.feature.customHotkeys": "Atajos personalizados",
  "pro.feature.customHotkeys.benefit": "Define tus propios atajos globales para la lista de apps y Quick Search.",
  "pro.prompt.customHotkeys": "Los atajos personalizados son parte de Pro. Desbloquea Pro para definir atajos globales.",
  "hotkeys.customize": "Personalizar",
  "hotkeys.restoreDefault": "Restaurar predeterminado",
  "hotkeys.recording": "Pulsa ahora el nuevo atajo. Pulsa Esc para cancelar.",
  "hotkeys.recordingInline": "Grabando",
  "hotkeys.recordingCancelled": "Grabación del atajo cancelada.",
  "hotkeys.saved": "Atajo guardado.",
  "hotkeys.restored": "Atajo predeterminado restaurado.",
  "hotkeys.proRequired": "Desbloquea Pro para personalizar atajos.",
  "hotkeys.error.modifierRequired": "Usa al menos Command, Control o dos modificadores.",
  "hotkeys.error.unsupportedKey": "Esta tecla no es compatible.",
  "hotkeys.error.reserved": "Este atajo está reservado por macOS o por la app.",
  "hotkeys.error.duplicate": "Este atajo ya pertenece a otra acción de TagLauncher.",
  "hotkeys.error.registrationFailed": "No se pudo registrar el atajo. Código de error: %status%."
}
src/Apptag/Localization/fr.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Tous les thèmes sont débloqués sur ce Mac.",
  "theme.tuning.dark": "Réglage coloré sombre",
  "theme.tuning.light": "Réglage coloré clair",
  "theme.tuning.desc": "Ajustez le flux de couleurs liquide. Le style clair ou sombre du thème reste conservé. Les thèmes ne modifient que l’arrière-plan de la grille d’apps. Les conteneurs gardent une surface vitrée uniforme."
  "theme.tuning.desc": "Ajustez le flux de couleurs liquide. Le style clair ou sombre du thème reste conservé. Les thèmes ne modifient que l’arrière-plan de la grille d’apps. Les conteneurs gardent une surface vitrée uniforme.",
  "pro.feature.customHotkeys": "Raccourcis personnalisés",
  "pro.feature.customHotkeys.benefit": "Définissez vos raccourcis globaux pour la liste d’apps et Quick Search.",
  "pro.prompt.customHotkeys": "Les raccourcis personnalisés font partie de Pro. Déverrouillez Pro pour définir vos raccourcis globaux.",
  "hotkeys.customize": "Personnaliser",
  "hotkeys.restoreDefault": "Rétablir par défaut",
  "hotkeys.recording": "Appuyez maintenant sur le nouveau raccourci. Esc pour annuler.",
  "hotkeys.recordingInline": "Enregistrement",
  "hotkeys.recordingCancelled": "Enregistrement du raccourci annulé.",
  "hotkeys.saved": "Raccourci enregistré.",
  "hotkeys.restored": "Raccourci par défaut rétabli.",
  "hotkeys.proRequired": "Déverrouillez Pro pour personnaliser les raccourcis.",
  "hotkeys.error.modifierRequired": "Utilisez au moins Command, Control ou deux modificateurs.",
  "hotkeys.error.unsupportedKey": "Cette touche n’est pas prise en charge.",
  "hotkeys.error.reserved": "Ce raccourci est réservé par macOS ou par l’app.",
  "hotkeys.error.duplicate": "Ce raccourci est déjà utilisé par une autre action TagLauncher.",
  "hotkeys.error.registrationFailed": "Le raccourci n’a pas pu être enregistré. Code d’erreur : %status%."
}
src/Apptag/Localization/id.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Semua tema tidak terkunci di Mac ini.",
  "theme.tuning.dark": "Setelan warna gelap",
  "theme.tuning.light": "Setelan warna terang",
  "theme.tuning.desc": "Atur aliran warna cair. Tema tetap mempertahankan gaya terang atau gelapnya. Tema hanya mengubah latar belakang grid app. Kontainer tetap memakai permukaan kaca yang konsisten."
  "theme.tuning.desc": "Atur aliran warna cair. Tema tetap mempertahankan gaya terang atau gelapnya. Tema hanya mengubah latar belakang grid app. Kontainer tetap memakai permukaan kaca yang konsisten.",
  "pro.feature.customHotkeys": "Pintasan khusus",
  "pro.feature.customHotkeys.benefit": "Atur pintasan global untuk daftar app dan Quick Search.",
  "pro.prompt.customHotkeys": "Pintasan khusus adalah fitur Pro. Buka Pro untuk mengatur pintasan global.",
  "hotkeys.customize": "Sesuaikan",
  "hotkeys.restoreDefault": "Pulihkan bawaan",
  "hotkeys.recording": "Tekan pintasan baru sekarang. Tekan Esc untuk membatalkan.",
  "hotkeys.recordingInline": "Merekam",
  "hotkeys.recordingCancelled": "Perekaman pintasan dibatalkan.",
  "hotkeys.saved": "Pintasan disimpan.",
  "hotkeys.restored": "Pintasan bawaan dipulihkan.",
  "hotkeys.proRequired": "Buka Pro untuk menyesuaikan pintasan.",
  "hotkeys.error.modifierRequired": "Gunakan setidaknya Command, Control, atau dua tombol pengubah.",
  "hotkeys.error.unsupportedKey": "Tombol ini tidak didukung.",
  "hotkeys.error.reserved": "Pintasan ini dicadangkan oleh macOS atau app.",
  "hotkeys.error.duplicate": "Pintasan ini sudah dipakai oleh tindakan TagLauncher lain.",
  "hotkeys.error.registrationFailed": "Pintasan tidak dapat didaftarkan. Kode kesalahan: %status%."
}
src/Apptag/Localization/it.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Tutti i temi sono sbloccati su questo Mac.",
  "theme.tuning.dark": "Regola colorato scuro",
  "theme.tuning.light": "Regola colorato chiaro",
  "theme.tuning.desc": "Regola il flusso liquido dei colori. Il tema mantiene lo stile chiaro o scuro corrente. I temi cambiano solo lo sfondo della griglia delle app. I contenitori mantengono una superficie in vetro uniforme."
  "theme.tuning.desc": "Regola il flusso liquido dei colori. Il tema mantiene lo stile chiaro o scuro corrente. I temi cambiano solo lo sfondo della griglia delle app. I contenitori mantengono una superficie in vetro uniforme.",
  "pro.feature.customHotkeys": "Scorciatoie personalizzate",
  "pro.feature.customHotkeys.benefit": "Imposta scorciatoie globali per elenco app e Quick Search.",
  "pro.prompt.customHotkeys": "Le scorciatoie personalizzate fanno parte di Pro. Sblocca Pro per impostarle.",
  "hotkeys.customize": "Personalizza",
  "hotkeys.restoreDefault": "Ripristina predefinito",
  "hotkeys.recording": "Premi ora la nuova scorciatoia. Premi Esc per annullare.",
  "hotkeys.recordingInline": "Registrazione",
  "hotkeys.recordingCancelled": "Registrazione scorciatoia annullata.",
  "hotkeys.saved": "Scorciatoia salvata.",
  "hotkeys.restored": "Scorciatoia predefinita ripristinata.",
  "hotkeys.proRequired": "Sblocca Pro per personalizzare le scorciatoie.",
  "hotkeys.error.modifierRequired": "Usa almeno Command, Control o due modificatori.",
  "hotkeys.error.unsupportedKey": "Questo tasto non è supportato.",
  "hotkeys.error.reserved": "Questa scorciatoia è riservata da macOS o dall’app.",
  "hotkeys.error.duplicate": "Questa scorciatoia è già usata da un’altra azione TagLauncher.",
  "hotkeys.error.registrationFailed": "Impossibile registrare la scorciatoia. Codice errore: %status%."
}
src/Apptag/Localization/ja.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "この Mac ではすべてのテーマのロックが解除されています。",
  "theme.tuning.dark": "ダークカラフル調整",
  "theme.tuning.light": "ライトカラフル調整",
  "theme.tuning.desc": "液体のような色の流れを調整します。テーマの明暗スタイルは保たれます。テーマはアプリグリッドの背景だけを変更します。コンテナは統一されたガラス面のままです。"
  "theme.tuning.desc": "液体のような色の流れを調整します。テーマの明暗スタイルは保たれます。テーマはアプリグリッドの背景だけを変更します。コンテナは統一されたガラス面のままです。",
  "pro.feature.customHotkeys": "カスタムショートカット",
  "pro.feature.customHotkeys.benefit": "アプリ一覧と Quick Search のグローバルショートカットを設定できます。",
  "pro.prompt.customHotkeys": "カスタムショートカットは Pro 機能です。Pro を解除すると設定できます。",
  "hotkeys.customize": "カスタマイズ",
  "hotkeys.restoreDefault": "デフォルトに戻す",
  "hotkeys.recording": "新しいショートカットを押してください。Esc でキャンセル。",
  "hotkeys.recordingInline": "録画中",
  "hotkeys.recordingCancelled": "ショートカットの記録をキャンセルしました。",
  "hotkeys.saved": "ショートカットを保存しました。",
  "hotkeys.restored": "デフォルトのショートカットに戻しました。",
  "hotkeys.proRequired": "ショートカットのカスタマイズには Pro が必要です。",
  "hotkeys.error.modifierRequired": "Command、Control、または 2 つ以上の修飾キーを使ってください。",
  "hotkeys.error.unsupportedKey": "このキーはサポートされていません。",
  "hotkeys.error.reserved": "このショートカットは macOS またはアプリで予約されています。",
  "hotkeys.error.duplicate": "このショートカットは別の TagLauncher 操作で使用済みです。",
  "hotkeys.error.registrationFailed": "ショートカットを登録できませんでした。エラーコード:%status%。"
}
src/Apptag/Localization/ko.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "모든 테마는 이 Mac에서 잠금 해제됩니다.",
  "theme.tuning.dark": "어두운 컬러풀 조정",
  "theme.tuning.light": "밝은 컬러풀 조정",
  "theme.tuning.desc": "흐르는 액체 색감을 조정합니다. 테마의 밝기 스타일은 유지됩니다. 테마는 앱 그리드 배경만 바꿉니다. 컨테이너는 일관된 유리 표면을 유지합니다."
  "theme.tuning.desc": "흐르는 액체 색감을 조정합니다. 테마의 밝기 스타일은 유지됩니다. 테마는 앱 그리드 배경만 바꿉니다. 컨테이너는 일관된 유리 표면을 유지합니다.",
  "pro.feature.customHotkeys": "사용자 지정 단축키",
  "pro.feature.customHotkeys.benefit": "앱 목록과 Quick Search의 전역 단축키를 직접 설정합니다.",
  "pro.prompt.customHotkeys": "사용자 지정 단축키는 Pro 기능입니다. Pro를 잠금 해제해 전역 단축키를 설정하세요.",
  "hotkeys.customize": "사용자 지정",
  "hotkeys.restoreDefault": "기본값 복원",
  "hotkeys.recording": "새 단축키를 지금 누르세요. Esc로 취소합니다.",
  "hotkeys.recordingInline": "기록 중",
  "hotkeys.recordingCancelled": "단축키 기록이 취소되었습니다.",
  "hotkeys.saved": "단축키가 저장되었습니다.",
  "hotkeys.restored": "기본 단축키가 복원되었습니다.",
  "hotkeys.proRequired": "단축키를 사용자 지정하려면 Pro가 필요합니다.",
  "hotkeys.error.modifierRequired": "Command, Control 또는 두 개 이상의 보조 키를 사용하세요.",
  "hotkeys.error.unsupportedKey": "이 키는 지원되지 않습니다.",
  "hotkeys.error.reserved": "이 단축키는 macOS 또는 앱에서 예약되어 있습니다.",
  "hotkeys.error.duplicate": "이 단축키는 다른 TagLauncher 동작에 이미 사용 중입니다.",
  "hotkeys.error.registrationFailed": "단축키를 등록할 수 없습니다. 오류 코드: %status%."
}
src/Apptag/Localization/ms.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Semua tema dibuka kunci pada Mac ini.",
  "theme.tuning.dark": "Larasan berwarna gelap",
  "theme.tuning.light": "Larasan berwarna cerah",
  "theme.tuning.desc": "Laraskan aliran warna cecair. Tema mengekalkan gaya cerah atau gelap semasa. Tema hanya menukar latar belakang grid app. Kontainer kekal dengan permukaan kaca yang konsisten."
  "theme.tuning.desc": "Laraskan aliran warna cecair. Tema mengekalkan gaya cerah atau gelap semasa. Tema hanya menukar latar belakang grid app. Kontainer kekal dengan permukaan kaca yang konsisten.",
  "pro.feature.customHotkeys": "Pintasan tersuai",
  "pro.feature.customHotkeys.benefit": "Tetapkan pintasan global untuk senarai app dan Quick Search.",
  "pro.prompt.customHotkeys": "Pintasan tersuai ialah ciri Pro. Buka Pro untuk menetapkan pintasan global.",
  "hotkeys.customize": "Sesuaikan",
  "hotkeys.restoreDefault": "Pulihkan lalai",
  "hotkeys.recording": "Tekan pintasan baharu sekarang. Tekan Esc untuk batal.",
  "hotkeys.recordingInline": "Merakam",
  "hotkeys.recordingCancelled": "Rakaman pintasan dibatalkan.",
  "hotkeys.saved": "Pintasan disimpan.",
  "hotkeys.restored": "Pintasan lalai dipulihkan.",
  "hotkeys.proRequired": "Buka Pro untuk menyesuaikan pintasan.",
  "hotkeys.error.modifierRequired": "Gunakan sekurang-kurangnya Command, Control atau dua pengubah.",
  "hotkeys.error.unsupportedKey": "Kekunci ini tidak disokong.",
  "hotkeys.error.reserved": "Pintasan ini dikhaskan oleh macOS atau app.",
  "hotkeys.error.duplicate": "Pintasan ini sudah digunakan oleh tindakan TagLauncher lain.",
  "hotkeys.error.registrationFailed": "Pintasan tidak dapat didaftarkan. Kod ralat: %status%."
}
src/Apptag/Localization/nb.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac.",
  "theme.tuning.dark": "Juster mørk fargerik",
  "theme.tuning.light": "Juster lys fargerik",
  "theme.tuning.desc": "Juster den flytende fargeflyten. Temaet beholder sin lyse eller mørke stil. Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate."
  "theme.tuning.desc": "Juster den flytende fargeflyten. Temaet beholder sin lyse eller mørke stil. Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate.",
  "pro.feature.customHotkeys": "Egne snarveier",
  "pro.feature.customHotkeys.benefit": "Angi globale snarveier for applisten og Quick Search.",
  "pro.prompt.customHotkeys": "Egne snarveier er en Pro-funksjon. Lås opp Pro for å angi dem.",
  "hotkeys.customize": "Tilpass",
  "hotkeys.restoreDefault": "Gjenopprett standard",
  "hotkeys.recording": "Trykk den nye snarveien nå. Esc avbryter.",
  "hotkeys.recordingInline": "Tar opp",
  "hotkeys.recordingCancelled": "Opptak av snarvei avbrutt.",
  "hotkeys.saved": "Snarvei lagret.",
  "hotkeys.restored": "Standardsnarvei gjenopprettet.",
  "hotkeys.proRequired": "Lås opp Pro for å tilpasse snarveier.",
  "hotkeys.error.modifierRequired": "Bruk minst Command, Control eller to modifikatortaster.",
  "hotkeys.error.unsupportedKey": "Denne tasten støttes ikke.",
  "hotkeys.error.reserved": "Denne snarveien er reservert av macOS eller appen.",
  "hotkeys.error.duplicate": "Denne snarveien tilhører allerede en annen TagLauncher-handling.",
  "hotkeys.error.registrationFailed": "Snarveien kunne ikke registreres. Feilkode: %status%."
}
src/Apptag/Localization/nl.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alle thema's zijn ontgrendeld op deze Mac.",
  "theme.tuning.dark": "Donker kleurrijk afstellen",
  "theme.tuning.light": "Licht kleurrijk afstellen",
  "theme.tuning.desc": "Pas de vloeiende kleurstroom aan. Het thema behoudt zijn lichte of donkere stijl. Thema’s wijzigen alleen de achtergrond van het app-raster. Containers behouden één consistente glaslaag."
  "theme.tuning.desc": "Pas de vloeiende kleurstroom aan. Het thema behoudt zijn lichte of donkere stijl. Thema’s wijzigen alleen de achtergrond van het app-raster. Containers behouden één consistente glaslaag.",
  "pro.feature.customHotkeys": "Aangepaste sneltoetsen",
  "pro.feature.customHotkeys.benefit": "Stel eigen globale sneltoetsen in voor de applijst en Quick Search.",
  "pro.prompt.customHotkeys": "Aangepaste sneltoetsen horen bij Pro. Ontgrendel Pro om ze in te stellen.",
  "hotkeys.customize": "Aanpassen",
  "hotkeys.restoreDefault": "Standaard herstellen",
  "hotkeys.recording": "Druk nu op de nieuwe sneltoets. Druk op Esc om te annuleren.",
  "hotkeys.recordingInline": "Opnemen",
  "hotkeys.recordingCancelled": "Sneltoetsopname geannuleerd.",
  "hotkeys.saved": "Sneltoets opgeslagen.",
  "hotkeys.restored": "Standaardsneltoets hersteld.",
  "hotkeys.proRequired": "Ontgrendel Pro om sneltoetsen aan te passen.",
  "hotkeys.error.modifierRequired": "Gebruik minstens Command, Control of twee wijzigingstoetsen.",
  "hotkeys.error.unsupportedKey": "Deze toets wordt niet ondersteund.",
  "hotkeys.error.reserved": "Deze sneltoets is gereserveerd door macOS of de app.",
  "hotkeys.error.duplicate": "Deze sneltoets hoort al bij een andere TagLauncher-actie.",
  "hotkeys.error.registrationFailed": "Sneltoets kon niet worden geregistreerd. Foutcode: %status%."
}
src/Apptag/Localization/nn.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac.",
  "theme.tuning.dark": "Juster mørk fargerik",
  "theme.tuning.light": "Juster lys fargerik",
  "theme.tuning.desc": "Juster den flytande fargeflyten. Temaet held på den lyse eller mørke stilen. Tema endrar berre bakgrunnen i apprutenettet. Behaldarane har éi jamn glasflate."
  "theme.tuning.desc": "Juster den flytande fargeflyten. Temaet held på den lyse eller mørke stilen. Tema endrar berre bakgrunnen i apprutenettet. Behaldarane har éi jamn glasflate.",
  "pro.feature.customHotkeys": "Eigne snarvegar",
  "pro.feature.customHotkeys.benefit": "Set globale snarvegar for applista og Quick Search.",
  "pro.prompt.customHotkeys": "Eigne snarvegar er ein Pro-funksjon. Lås opp Pro for å setje dei.",
  "hotkeys.customize": "Tilpass",
  "hotkeys.restoreDefault": "Gjenopprett standard",
  "hotkeys.recording": "Trykk den nye snarveien nå. Esc avbryter.",
  "hotkeys.recordingInline": "Tar opp",
  "hotkeys.recordingCancelled": "Opptak av snarvei avbrutt.",
  "hotkeys.saved": "Snarveg lagra.",
  "hotkeys.restored": "Standardsnarveg gjenoppretta.",
  "hotkeys.proRequired": "Lås opp Pro for å tilpasse snarveier.",
  "hotkeys.error.modifierRequired": "Bruk minst Command, Control eller to modifikatortaster.",
  "hotkeys.error.unsupportedKey": "Denne tasten støttes ikke.",
  "hotkeys.error.reserved": "Denne snarveien er reservert av macOS eller appen.",
  "hotkeys.error.duplicate": "Denne snarveien tilhører allerede en annen TagLauncher-handling.",
  "hotkeys.error.registrationFailed": "Snarveien kunne ikke registreres. Feilkode: %status%."
}
src/Apptag/Localization/no.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac.",
  "theme.tuning.dark": "Juster mørk fargerik",
  "theme.tuning.light": "Juster lys fargerik",
  "theme.tuning.desc": "Juster den flytende fargeflyten. Temaet beholder sin lyse eller mørke stil. Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate."
  "theme.tuning.desc": "Juster den flytende fargeflyten. Temaet beholder sin lyse eller mørke stil. Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate.",
  "pro.feature.customHotkeys": "Egne snarveier",
  "pro.feature.customHotkeys.benefit": "Angi globale snarveier for applisten og Quick Search.",
  "pro.prompt.customHotkeys": "Egne snarveier er en Pro-funksjon. Lås opp Pro for å angi dem.",
  "hotkeys.customize": "Tilpass",
  "hotkeys.restoreDefault": "Gjenopprett standard",
  "hotkeys.recording": "Trykk den nye snarveien nå. Esc avbryter.",
  "hotkeys.recordingInline": "Tar opp",
  "hotkeys.recordingCancelled": "Opptak av snarvei avbrutt.",
  "hotkeys.saved": "Snarvei lagret.",
  "hotkeys.restored": "Standardsnarvei gjenopprettet.",
  "hotkeys.proRequired": "Lås opp Pro for å tilpasse snarveier.",
  "hotkeys.error.modifierRequired": "Bruk minst Command, Control eller to modifikatortaster.",
  "hotkeys.error.unsupportedKey": "Denne tasten støttes ikke.",
  "hotkeys.error.reserved": "Denne snarveien er reservert av macOS eller appen.",
  "hotkeys.error.duplicate": "Denne snarveien tilhører allerede en annen TagLauncher-handling.",
  "hotkeys.error.registrationFailed": "Snarveien kunne ikke registreres. Feilkode: %status%."
}
src/Apptag/Localization/pl.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Wszystkie motywy są odblokowane na tym Mac.",
  "theme.tuning.dark": "Regulacja ciemnego kolorowego",
  "theme.tuning.light": "Regulacja jasnego kolorowego",
  "theme.tuning.desc": "Dostosuj płynny przepływ kolorów. Motyw zachowuje jasny lub ciemny styl. Motywy zmieniają tylko tło siatki aplikacji. Kontenery zachowują jednolitą szklaną powierzchnię."
  "theme.tuning.desc": "Dostosuj płynny przepływ kolorów. Motyw zachowuje jasny lub ciemny styl. Motywy zmieniają tylko tło siatki aplikacji. Kontenery zachowują jednolitą szklaną powierzchnię.",
  "pro.feature.customHotkeys": "Własne skróty",
  "pro.feature.customHotkeys.benefit": "Ustaw globalne skróty dla listy aplikacji i Quick Search.",
  "pro.prompt.customHotkeys": "Własne skróty są częścią Pro. Odblokuj Pro, aby je ustawić.",
  "hotkeys.customize": "Dostosuj",
  "hotkeys.restoreDefault": "Przywróć domyślne",
  "hotkeys.recording": "Naciśnij teraz nowy skrót. Esc anuluje.",
  "hotkeys.recordingInline": "Nagrywanie",
  "hotkeys.recordingCancelled": "Nagrywanie skrótu anulowane.",
  "hotkeys.saved": "Skrót zapisany.",
  "hotkeys.restored": "Domyślny skrót przywrócony.",
  "hotkeys.proRequired": "Odblokuj Pro, aby dostosować skróty.",
  "hotkeys.error.modifierRequired": "Użyj co najmniej Command, Control albo dwóch modyfikatorów.",
  "hotkeys.error.unsupportedKey": "Ten klawisz nie jest obsługiwany.",
  "hotkeys.error.reserved": "Ten skrót jest zarezerwowany przez macOS lub aplikację.",
  "hotkeys.error.duplicate": "Ten skrót jest już przypisany do innej akcji TagLauncher.",
  "hotkeys.error.registrationFailed": "Nie można zarejestrować skrótu. Kod błędu: %status%."
}
src/Apptag/Localization/pt-BR.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Todos os temas são desbloqueados neste Mac.",
  "theme.tuning.dark": "Ajuste colorido escuro",
  "theme.tuning.light": "Ajuste colorido claro",
  "theme.tuning.desc": "Ajuste o fluxo líquido de cores. O tema mantém o estilo claro ou escuro atual. Os temas mudam apenas o fundo da grade de apps. Os contêineres mantêm uma superfície de vidro consistente."
  "theme.tuning.desc": "Ajuste o fluxo líquido de cores. O tema mantém o estilo claro ou escuro atual. Os temas mudam apenas o fundo da grade de apps. Os contêineres mantêm uma superfície de vidro consistente.",
  "pro.feature.customHotkeys": "Atalhos personalizados",
  "pro.feature.customHotkeys.benefit": "Defina atalhos globais para a lista de apps e o Quick Search.",
  "pro.prompt.customHotkeys": "Atalhos personalizados fazem parte do Pro. Desbloqueie o Pro para definir atalhos globais.",
  "hotkeys.customize": "Personalizar",
  "hotkeys.restoreDefault": "Restaurar padrão",
  "hotkeys.recording": "Pressione o novo atalho agora. Pressione Esc para cancelar.",
  "hotkeys.recordingInline": "Gravando",
  "hotkeys.recordingCancelled": "Gravação do atalho cancelada.",
  "hotkeys.saved": "Atalho salvo.",
  "hotkeys.restored": "Atalho padrão restaurado.",
  "hotkeys.proRequired": "Desbloqueie o Pro para personalizar atalhos.",
  "hotkeys.error.modifierRequired": "Use pelo menos Command, Control ou dois modificadores.",
  "hotkeys.error.unsupportedKey": "Esta tecla não é compatível.",
  "hotkeys.error.reserved": "Este atalho é reservado pelo macOS ou pelo app.",
  "hotkeys.error.duplicate": "Este atalho já pertence a outra ação do TagLauncher.",
  "hotkeys.error.registrationFailed": "Não foi possível registrar o atalho. Código de erro: %status%."
}
src/Apptag/Localization/ro.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Toate temele sunt deblocate pe acest Mac.",
  "theme.tuning.dark": "Ajustare colorat întunecat",
  "theme.tuning.light": "Ajustare colorat luminos",
  "theme.tuning.desc": "Ajustează fluxul lichid de culori. Tema păstrează stilul luminos sau întunecat curent. Temele schimbă doar fundalul grilei de aplicații. Containerele păstrează o suprafață de sticlă uniformă."
  "theme.tuning.desc": "Ajustează fluxul lichid de culori. Tema păstrează stilul luminos sau întunecat curent. Temele schimbă doar fundalul grilei de aplicații. Containerele păstrează o suprafață de sticlă uniformă.",
  "pro.feature.customHotkeys": "Scurtături personalizate",
  "pro.feature.customHotkeys.benefit": "Setează scurtături globale pentru lista de aplicații și Quick Search.",
  "pro.prompt.customHotkeys": "Scurtăturile personalizate fac parte din Pro. Deblochează Pro pentru a le seta.",
  "hotkeys.customize": "Personalizează",
  "hotkeys.restoreDefault": "Revino la implicit",
  "hotkeys.recording": "Apasă acum noua scurtătură. Esc anulează.",
  "hotkeys.recordingInline": "Se înregistrează",
  "hotkeys.recordingCancelled": "Înregistrarea scurtăturii a fost anulată.",
  "hotkeys.saved": "Scurtătură salvată.",
  "hotkeys.restored": "Scurtătura implicită a fost restaurată.",
  "hotkeys.proRequired": "Deblochează Pro pentru a personaliza scurtături.",
  "hotkeys.error.modifierRequired": "Folosește cel puțin Command, Control sau doi modificatori.",
  "hotkeys.error.unsupportedKey": "Această tastă nu este acceptată.",
  "hotkeys.error.reserved": "Această scurtătură este rezervată de macOS sau aplicație.",
  "hotkeys.error.duplicate": "Această scurtătură aparține deja altei acțiuni TagLauncher.",
  "hotkeys.error.registrationFailed": "Scurtătura nu a putut fi înregistrată. Cod eroare: %status%."
}
src/Apptag/Localization/ru.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "На этом Mac разблокированы все темы.",
  "theme.tuning.dark": "Настройка тёмного цветного",
  "theme.tuning.light": "Настройка светлого цветного",
  "theme.tuning.desc": "Настройте текучий цветовой переход. Тема сохраняет свой светлый или тёмный стиль. Темы меняют только фон сетки приложений. Контейнеры сохраняют единый стеклянный вид."
  "theme.tuning.desc": "Настройте текучий цветовой переход. Тема сохраняет свой светлый или тёмный стиль. Темы меняют только фон сетки приложений. Контейнеры сохраняют единый стеклянный вид.",
  "pro.feature.customHotkeys": "Свои сочетания клавиш",
  "pro.feature.customHotkeys.benefit": "Настройте глобальные сочетания для списка приложений и Quick Search.",
  "pro.prompt.customHotkeys": "Свои сочетания клавиш входят в Pro. Откройте Pro, чтобы настроить их.",
  "hotkeys.customize": "Настроить",
  "hotkeys.restoreDefault": "Вернуть стандартное",
  "hotkeys.recording": "Нажмите новое сочетание. Esc — отмена.",
  "hotkeys.recordingInline": "Запись",
  "hotkeys.recordingCancelled": "Запись сочетания отменена.",
  "hotkeys.saved": "Сочетание сохранено.",
  "hotkeys.restored": "Стандартное сочетание восстановлено.",
  "hotkeys.proRequired": "Откройте Pro, чтобы настраивать сочетания.",
  "hotkeys.error.modifierRequired": "Используйте Command, Control или две клавиши-модификатора.",
  "hotkeys.error.unsupportedKey": "Эта клавиша не поддерживается.",
  "hotkeys.error.reserved": "Это сочетание зарезервировано macOS или приложением.",
  "hotkeys.error.duplicate": "Это сочетание уже используется другим действием TagLauncher.",
  "hotkeys.error.registrationFailed": "Не удалось зарегистрировать сочетание. Код ошибки: %status%."
}
src/Apptag/Localization/sr-Cyrl.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Све теме су откључане на овом Mac-у.",
  "theme.tuning.dark": "Подешавање тамно шареног",
  "theme.tuning.light": "Подешавање светло шареног",
  "theme.tuning.desc": "Подесите течни прелаз боја. Тема задржава свој светли или тамни стил. Теме мењају само позадину мреже апликација. Контејнери задржавају једноличну стаклену површину."
  "theme.tuning.desc": "Подесите течни прелаз боја. Тема задржава свој светли или тамни стил. Теме мењају само позадину мреже апликација. Контејнери задржавају једноличну стаклену површину.",
  "pro.feature.customHotkeys": "Прилагођене пречице",
  "pro.feature.customHotkeys.benefit": "Подесите глобалне пречице за листу апликација и Quick Search.",
  "pro.prompt.customHotkeys": "Прилагођене пречице су Pro функција. Откључајте Pro да их подесите.",
  "hotkeys.customize": "Прилагоди",
  "hotkeys.restoreDefault": "Врати подразумевано",
  "hotkeys.recording": "Притисните нову пречицу. Esc отказује.",
  "hotkeys.recordingInline": "Снимање",
  "hotkeys.recordingCancelled": "Снимање пречице је отказано.",
  "hotkeys.saved": "Пречица је сачувана.",
  "hotkeys.restored": "Подразумевана пречица је враћена.",
  "hotkeys.proRequired": "Откључајте Pro да прилагодите пречице.",
  "hotkeys.error.modifierRequired": "Користите бар Command, Control или два модификатора.",
  "hotkeys.error.unsupportedKey": "Овај тастер није подржан.",
  "hotkeys.error.reserved": "Ова пречица је резервисана од macOS-а или апликације.",
  "hotkeys.error.duplicate": "Ова пречица већ припада другој TagLauncher радњи.",
  "hotkeys.error.registrationFailed": "Пречица није могла да се региструје. Код грешке: %status%."
}
src/Apptag/Localization/sv.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Alla teman är upplåsta på denna Mac.",
  "theme.tuning.dark": "Justera mörkt färgstark",
  "theme.tuning.light": "Justera ljust färgstark",
  "theme.tuning.desc": "Justera det flytande färgflödet. Temat behåller sin ljusa eller mörka stil. Teman ändrar bara bakgrunden i apprutnätet. Behållare har en enhetlig glasyta."
  "theme.tuning.desc": "Justera det flytande färgflödet. Temat behåller sin ljusa eller mörka stil. Teman ändrar bara bakgrunden i apprutnätet. Behållare har en enhetlig glasyta.",
  "pro.feature.customHotkeys": "Egna kortkommandon",
  "pro.feature.customHotkeys.benefit": "Ställ in globala kortkommandon för applistan och Quick Search.",
  "pro.prompt.customHotkeys": "Egna kortkommandon ingår i Pro. Lås upp Pro för att ställa in dem.",
  "hotkeys.customize": "Anpassa",
  "hotkeys.restoreDefault": "Återställ standard",
  "hotkeys.recording": "Tryck på det nya kortkommandot nu. Esc avbryter.",
  "hotkeys.recordingInline": "Spelar in",
  "hotkeys.recordingCancelled": "Inspelning av kortkommando avbröts.",
  "hotkeys.saved": "Kortkommando sparat.",
  "hotkeys.restored": "Standardkortkommando återställt.",
  "hotkeys.proRequired": "Lås upp Pro för att anpassa kortkommandon.",
  "hotkeys.error.modifierRequired": "Använd minst Command, Control eller två modifierare.",
  "hotkeys.error.unsupportedKey": "Den här tangenten stöds inte.",
  "hotkeys.error.reserved": "Det här kortkommandot är reserverat av macOS eller appen.",
  "hotkeys.error.duplicate": "Det här kortkommandot används redan av en annan TagLauncher-åtgärd.",
  "hotkeys.error.registrationFailed": "Kortkommandot kunde inte registreras. Felkod: %status%."
}
src/Apptag/Localization/th.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "ปลดล็อคธีมทั้งหมดแล้วใน Mac นี้",
  "theme.tuning.dark": "ปรับสีสันโทนมืด",
  "theme.tuning.light": "ปรับสีสันโทนสว่าง",
  "theme.tuning.desc": "ปรับการไหลของสีแบบของเหลว โดยยังคงสไตล์สว่างหรือมืดของธีมไว้ ธีมจะเปลี่ยนเฉพาะพื้นหลังของกริดแอป คอนเทนเนอร์ยังคงเป็นพื้นผิวกระจกแบบเดียวกัน"
  "theme.tuning.desc": "ปรับการไหลของสีแบบของเหลว โดยยังคงสไตล์สว่างหรือมืดของธีมไว้ ธีมจะเปลี่ยนเฉพาะพื้นหลังของกริดแอป คอนเทนเนอร์ยังคงเป็นพื้นผิวกระจกแบบเดียวกัน",
  "pro.feature.customHotkeys": "ปุ่มลัดกำหนดเอง",
  "pro.feature.customHotkeys.benefit": "ตั้งปุ่มลัดส่วนกลางสำหรับรายการแอปและ Quick Search",
  "pro.prompt.customHotkeys": "ปุ่มลัดกำหนดเองเป็นฟีเจอร์ Pro ปลดล็อก Pro เพื่อตั้งค่าได้",
  "hotkeys.customize": "กำหนดเอง",
  "hotkeys.restoreDefault": "คืนค่าเริ่มต้น",
  "hotkeys.recording": "กดปุ่มลัดใหม่ตอนนี้ กด Esc เพื่อยกเลิก",
  "hotkeys.recordingInline": "กำลังบันทึก",
  "hotkeys.recordingCancelled": "ยกเลิกการบันทึกปุ่มลัดแล้ว",
  "hotkeys.saved": "บันทึกปุ่มลัดแล้ว",
  "hotkeys.restored": "คืนค่าปุ่มลัดเริ่มต้นแล้ว",
  "hotkeys.proRequired": "ปลดล็อก Pro เพื่อกำหนดปุ่มลัดเอง",
  "hotkeys.error.modifierRequired": "ใช้ Command, Control หรือปุ่มปรับแต่งอย่างน้อยสองปุ่ม",
  "hotkeys.error.unsupportedKey": "ไม่รองรับปุ่มนี้",
  "hotkeys.error.reserved": "ปุ่มลัดนี้ถูกสงวนไว้โดย macOS หรือแอป",
  "hotkeys.error.duplicate": "ปุ่มลัดนี้ถูกใช้กับการทำงานอื่นของ TagLauncher แล้ว",
  "hotkeys.error.registrationFailed": "ไม่สามารถลงทะเบียนปุ่มลัดได้ รหัสข้อผิดพลาด: %status%"
}
src/Apptag/Localization/tr.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Bu Mac üzerinde tüm temaların kilidi açıldı.",
  "theme.tuning.dark": "Koyu renk ayarı",
  "theme.tuning.light": "Açık renk ayarı",
  "theme.tuning.desc": "Akışkan renk geçişini ayarlayın. Tema mevcut açık veya koyu stilini korur. Temalar yalnızca uygulama ızgarası arka planını değiştirir. Kapsayıcılar tutarlı bir cam yüzey olarak kalır."
  "theme.tuning.desc": "Akışkan renk geçişini ayarlayın. Tema mevcut açık veya koyu stilini korur. Temalar yalnızca uygulama ızgarası arka planını değiştirir. Kapsayıcılar tutarlı bir cam yüzey olarak kalır.",
  "pro.feature.customHotkeys": "Özel kısayollar",
  "pro.feature.customHotkeys.benefit": "Uygulama listesi ve Quick Search için genel kısayollar ayarlayın.",
  "pro.prompt.customHotkeys": "Özel kısayollar Pro özelliğidir. Genel kısayolları ayarlamak için Pro kilidini açın.",
  "hotkeys.customize": "Özelleştir",
  "hotkeys.restoreDefault": "Varsayılana dön",
  "hotkeys.recording": "Yeni kısayola şimdi basın. İptal için Esc.",
  "hotkeys.recordingInline": "Kaydediliyor",
  "hotkeys.recordingCancelled": "Kısayol kaydı iptal edildi.",
  "hotkeys.saved": "Kısayol kaydedildi.",
  "hotkeys.restored": "Varsayılan kısayol geri yüklendi.",
  "hotkeys.proRequired": "Kısayolları özelleştirmek için Pro kilidini açın.",
  "hotkeys.error.modifierRequired": "En az Command, Control veya iki değiştirici kullanın.",
  "hotkeys.error.unsupportedKey": "Bu tuş desteklenmiyor.",
  "hotkeys.error.reserved": "Bu kısayol macOS veya uygulama tarafından ayrılmıştır.",
  "hotkeys.error.duplicate": "Bu kısayol başka bir TagLauncher eylemine atanmış.",
  "hotkeys.error.registrationFailed": "Kısayol kaydedilemedi. Hata kodu: %status%."
}
src/Apptag/Localization/uk.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "усі теми розблоковано на цьому Mac.",
  "theme.tuning.dark": "Налаштування темного кольорового",
  "theme.tuning.light": "Налаштування світлого кольорового",
  "theme.tuning.desc": "Налаштуйте плавний потік кольорів. Тема зберігає свій світлий або темний стиль. Теми змінюють лише тло сітки програм. Контейнери зберігають єдину скляну поверхню."
  "theme.tuning.desc": "Налаштуйте плавний потік кольорів. Тема зберігає свій світлий або темний стиль. Теми змінюють лише тло сітки програм. Контейнери зберігають єдину скляну поверхню.",
  "pro.feature.customHotkeys": "Власні скорочення",
  "pro.feature.customHotkeys.benefit": "Налаштуйте глобальні скорочення для списку програм і Quick Search.",
  "pro.prompt.customHotkeys": "Власні скорочення входять до Pro. Розблокуйте Pro, щоб налаштувати їх.",
  "hotkeys.customize": "Налаштувати",
  "hotkeys.restoreDefault": "Відновити типове",
  "hotkeys.recording": "Натисніть нове скорочення. Esc — скасувати.",
  "hotkeys.recordingInline": "Запис",
  "hotkeys.recordingCancelled": "Запис скорочення скасовано.",
  "hotkeys.saved": "Скорочення збережено.",
  "hotkeys.restored": "Типове скорочення відновлено.",
  "hotkeys.proRequired": "Розблокуйте Pro, щоб налаштовувати скорочення.",
  "hotkeys.error.modifierRequired": "Використайте Command, Control або дві клавіші-модифікатори.",
  "hotkeys.error.unsupportedKey": "Ця клавіша не підтримується.",
  "hotkeys.error.reserved": "Це скорочення зарезервоване macOS або програмою.",
  "hotkeys.error.duplicate": "Це скорочення вже використовується іншою дією TagLauncher.",
  "hotkeys.error.registrationFailed": "Не вдалося зареєструвати скорочення. Код помилки: %status%."
}
src/Apptag/Localization/vi.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "Tất cả chủ đề đều được mở khóa trên Mac này.",
  "theme.tuning.dark": "Tinh chỉnh đa sắc tối",
  "theme.tuning.light": "Tinh chỉnh đa sắc sáng",
  "theme.tuning.desc": "Điều chỉnh dòng màu dạng lỏng. Chủ đề vẫn giữ phong cách sáng hoặc tối hiện tại. Chủ đề chỉ thay đổi nền của lưới ứng dụng. Các khung giữ một bề mặt kính thống nhất."
  "theme.tuning.desc": "Điều chỉnh dòng màu dạng lỏng. Chủ đề vẫn giữ phong cách sáng hoặc tối hiện tại. Chủ đề chỉ thay đổi nền của lưới ứng dụng. Các khung giữ một bề mặt kính thống nhất.",
  "pro.feature.customHotkeys": "Phím tắt tuỳ chỉnh",
  "pro.feature.customHotkeys.benefit": "Đặt phím tắt toàn cục cho danh sách ứng dụng và Quick Search.",
  "pro.prompt.customHotkeys": "Phím tắt tuỳ chỉnh là tính năng Pro. Mở khoá Pro để đặt phím tắt toàn cục.",
  "hotkeys.customize": "Tuỳ chỉnh",
  "hotkeys.restoreDefault": "Khôi phục mặc định",
  "hotkeys.recording": "Nhấn phím tắt mới ngay bây giờ. Nhấn Esc để huỷ.",
  "hotkeys.recordingInline": "Đang ghi",
  "hotkeys.recordingCancelled": "Đã huỷ ghi phím tắt.",
  "hotkeys.saved": "Đã lưu phím tắt.",
  "hotkeys.restored": "Đã khôi phục phím tắt mặc định.",
  "hotkeys.proRequired": "Mở khoá Pro để tuỳ chỉnh phím tắt.",
  "hotkeys.error.modifierRequired": "Dùng ít nhất Command, Control hoặc hai phím bổ trợ.",
  "hotkeys.error.unsupportedKey": "Phím này không được hỗ trợ.",
  "hotkeys.error.reserved": "Phím tắt này được macOS hoặc ứng dụng giữ lại.",
  "hotkeys.error.duplicate": "Phím tắt này đã thuộc về một thao tác TagLauncher khác.",
  "hotkeys.error.registrationFailed": "Không thể đăng ký phím tắt. Mã lỗi: %status%."
}
src/Apptag/Localization/zh-Hans.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "这台 Mac 上的全部主题都已解锁。",
  "theme.tuning.dark": "暗色炫彩调色",
  "theme.tuning.light": "亮色炫彩调色",
  "theme.tuning.desc": "调整背景的液体流动色彩,主题会保持当前的明暗风格。主题只改变应用网格背景。容器保持统一的玻璃面板。"
  "theme.tuning.desc": "调整背景的液体流动色彩,主题会保持当前的明暗风格。主题只改变应用网格背景。容器保持统一的玻璃面板。",
  "pro.feature.customHotkeys": "自定义快捷键",
  "pro.feature.customHotkeys.benefit": "为主界面和快捷搜索设置自己的全局快捷键。",
  "pro.prompt.customHotkeys": "自定义快捷键属于 Pro 功能。解锁 Pro 后即可设置自己的全局快捷键。",
  "hotkeys.customize": "自定义",
  "hotkeys.restoreDefault": "恢复默认",
  "hotkeys.recording": "请按下新的快捷键。按 Esc 取消。",
  "hotkeys.recordingInline": "录制中",
  "hotkeys.recordingCancelled": "已取消快捷键录制。",
  "hotkeys.saved": "快捷键已保存。",
  "hotkeys.restored": "已恢复默认快捷键。",
  "hotkeys.proRequired": "解锁 Pro 后可自定义快捷键。",
  "hotkeys.error.modifierRequired": "请至少使用 Command、Control,或两个组合键。",
  "hotkeys.error.unsupportedKey": "不支持这个按键。",
  "hotkeys.error.reserved": "这个快捷键已被 macOS 或应用保留。",
  "hotkeys.error.duplicate": "这个快捷键已用于 TagLauncher 的另一个操作。",
  "hotkeys.error.registrationFailed": "快捷键注册失败。错误码:%status%。"
}
src/Apptag/Localization/zh-Hant.json
@@ -297,5 +297,21 @@
  "pro.theme.status.unlocked": "這台 Mac 上的全部主題都已解鎖。",
  "theme.tuning.dark": "暗色炫彩調色",
  "theme.tuning.light": "亮色炫彩調色",
  "theme.tuning.desc": "調整背景的液體流動色彩,主題會保持目前的明暗風格。主題只會改變應用程式網格背景。容器會保持一致的玻璃面板。"
  "theme.tuning.desc": "調整背景的液體流動色彩,主題會保持目前的明暗風格。主題只會改變應用程式網格背景。容器會保持一致的玻璃面板。",
  "pro.feature.customHotkeys": "自訂快速鍵",
  "pro.feature.customHotkeys.benefit": "為主介面與快速搜尋設定自己的全域快速鍵。",
  "pro.prompt.customHotkeys": "自訂快速鍵屬於 Pro 功能。解鎖 Pro 後即可設定自己的全域快速鍵。",
  "hotkeys.customize": "自訂",
  "hotkeys.restoreDefault": "恢復預設",
  "hotkeys.recording": "請按下新的快速鍵。按 Esc 取消。",
  "hotkeys.recordingInline": "錄製中",
  "hotkeys.recordingCancelled": "已取消快速鍵錄製。",
  "hotkeys.saved": "快速鍵已儲存。",
  "hotkeys.restored": "已恢復預設快速鍵。",
  "hotkeys.proRequired": "解鎖 Pro 後可自訂快速鍵。",
  "hotkeys.error.modifierRequired": "請至少使用 Command、Control,或兩個組合鍵。",
  "hotkeys.error.unsupportedKey": "不支援這個按鍵。",
  "hotkeys.error.reserved": "這個快速鍵已被 macOS 或應用程式保留。",
  "hotkeys.error.duplicate": "這個快速鍵已用於 TagLauncher 的另一個操作。",
  "hotkeys.error.registrationFailed": "快速鍵註冊失敗。錯誤碼:%status%。"
}
src/Apptag/PreferencesView.swift
@@ -1,5 +1,6 @@
import SwiftUI
import AppKit
import Carbon
// MARK: - Preferences View
@@ -86,6 +87,9 @@
    @State private var isDataFilePanelPresented = false
    @State private var hotkeyStatusToast: String? = nil
    @State private var hotkeyStatusToastToken: UUID? = nil
    @State private var recordingHotkeyKind: LauncherHotkeyKind? = nil
    @State private var hotkeyRecorderMonitor: Any? = nil
    @State private var hotkeySettingsRefreshToken = UUID()
    @State private var selectedTab: SettingsTab = .general
    @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
    @State private var settingsProPromptFeature: ProFeature? = nil
@@ -305,8 +309,78 @@
        let status = LauncherHotkeyRegistrationStore.failureCode(for: kind)
            .map(String.init) ?? "-"
        return tr(key)
            .replacingOccurrences(of: "%shortcut%", with: kind.hotkey.displayString)
            .replacingOccurrences(of: "%shortcut%", with: LauncherHotkeySettings.effectiveHotkey(for: kind).displayString)
            .replacingOccurrences(of: "%status%", with: status)
    }
    private func startHotkeyRecording(for kind: LauncherHotkeyKind) {
        guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
            presentSettingsProPrompt(for: .customHotkeys)
            return
        }
        stopHotkeyRecording(showCancelledToast: false)
        recordingHotkeyKind = kind
        showHotkeyStatusToast(tr("hotkeys.recording"))
        hotkeyRecorderMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
            DispatchQueue.main.async {
                self.captureHotkeyEvent(event)
            }
            return nil
        }
    }
    private func captureHotkeyEvent(_ event: NSEvent) {
        guard let kind = recordingHotkeyKind else { return }
        if Int(event.keyCode) == kVK_Escape {
            stopHotkeyRecording(showCancelledToast: true)
            return
        }
        let candidate = LauncherHotkeySettings.candidate(from: event)
        let result = (NSApp.delegate as? AppDelegate)?
            .applyCustomHotkey(candidate, for: kind)
            ?? .registrationFailed(-1)
        stopHotkeyRecording(showCancelledToast: false)
        handleHotkeyCustomizationResult(result)
    }
    private func restoreDefaultHotkey(for kind: LauncherHotkeyKind) {
        guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
            presentSettingsProPrompt(for: .customHotkeys)
            return
        }
        let result = (NSApp.delegate as? AppDelegate)?
            .restoreDefaultHotkey(for: kind)
            ?? .registrationFailed(-1)
        handleHotkeyCustomizationResult(result)
    }
    private func stopHotkeyRecording(showCancelledToast: Bool = false) {
        if let hotkeyRecorderMonitor {
            NSEvent.removeMonitor(hotkeyRecorderMonitor)
        }
        hotkeyRecorderMonitor = nil
        recordingHotkeyKind = nil
        if showCancelledToast {
            showHotkeyStatusToast(tr("hotkeys.recordingCancelled"))
        }
    }
    private func handleHotkeyCustomizationResult(_ result: LauncherHotkeyCustomizationResult) {
        if case .locked = result {
            presentSettingsProPrompt(for: .customHotkeys)
            return
        }
        hotkeySettingsRefreshToken = UUID()
        showHotkeyStatusToast(hotkeyCustomizationMessage(for: result))
    }
    private func hotkeyCustomizationMessage(for result: LauncherHotkeyCustomizationResult) -> String {
        switch result {
        case .registrationFailed(let status):
            return tr(result.messageKey).replacingOccurrences(of: "%status%", with: String(status))
        default:
            return tr(result.messageKey)
        }
    }
    private func showHotkeyStatusToast(_ message: String) {
@@ -861,12 +935,18 @@
                .font(.system(size: 13, weight: .semibold))
                .foregroundStyle(.secondary)
            StaticHotkeyInfoRow(
            CustomizableHotkeyInfoRow(
                title: tr("quickSearch.mainHotkey"),
                description: tr("quickSearch.mainHotkeyDesc"),
                displayText: LauncherHotkey.main.displayString,
                displayText: LauncherHotkeySettings.effectiveHotkey(for: .main).displayString,
                statusText: hotkeyStatusText(for: .main),
                statusTone: hotkeyStatusTone(for: .main)
                statusTone: hotkeyStatusTone(for: .main),
                customizeTitle: proEntitlement.isUnlocked ? tr("hotkeys.customize") : tr("pro.card.badge"),
                restoreTitle: tr("hotkeys.restoreDefault"),
                isRecording: recordingHotkeyKind == .main,
                canRestore: proEntitlement.isUnlocked && LauncherHotkeySettings.hasCustomHotkey(for: .main),
                onCustomize: { startHotkeyRecording(for: .main) },
                onRestore: { restoreDefaultHotkey(for: .main) }
            )
            StaticHotkeyInfoRow(
@@ -877,12 +957,18 @@
                statusTone: .neutral
            )
            StaticHotkeyInfoRow(
            CustomizableHotkeyInfoRow(
                title: tr("quickSearch.globalHotkey"),
                description: tr("quickSearch.globalHotkeyDesc"),
                displayText: LauncherHotkey.quickSearch.displayString,
                displayText: LauncherHotkeySettings.effectiveHotkey(for: .quickSearch).displayString,
                statusText: hotkeyStatusText(for: .quickSearch),
                statusTone: hotkeyStatusTone(for: .quickSearch)
                statusTone: hotkeyStatusTone(for: .quickSearch),
                customizeTitle: proEntitlement.isUnlocked ? tr("hotkeys.customize") : tr("pro.card.badge"),
                restoreTitle: tr("hotkeys.restoreDefault"),
                isRecording: recordingHotkeyKind == .quickSearch,
                canRestore: proEntitlement.isUnlocked && LauncherHotkeySettings.hasCustomHotkey(for: .quickSearch),
                onCustomize: { startHotkeyRecording(for: .quickSearch) },
                onRestore: { restoreDefaultHotkey(for: .quickSearch) }
            )
            StaticHotkeyInfoRow(
@@ -893,6 +979,7 @@
                statusTone: .neutral
            )
        }
        .id(hotkeySettingsRefreshToken)
    }
    private var themeProStatusRow: some View {
@@ -1590,10 +1677,14 @@
            showPendingHotkeyWarningIfNeeded()
            refreshThemePreviewCountdown()
        }
        .onDisappear {
            stopHotkeyRecording()
        }
        .onReceive(themePreviewCountdownTimer) { now in
            refreshThemePreviewCountdown(now: now)
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in
            hotkeySettingsRefreshToken = UUID()
            showPendingHotkeyWarningIfNeeded()
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherPreferencesTabRequested)) { notification in
@@ -1676,6 +1767,79 @@
    }
}
private struct CustomizableHotkeyInfoRow: View {
    let title: String
    let description: String
    let displayText: String
    let statusText: String
    let statusTone: HotkeyStatusTone
    let customizeTitle: String
    let restoreTitle: String
    let isRecording: Bool
    let canRestore: Bool
    let onCustomize: () -> Void
    let onRestore: () -> Void
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(alignment: .top, spacing: 12) {
                VStack(alignment: .leading, spacing: 3) {
                    Text(title)
                        .font(.system(size: 13, weight: .semibold))
                    Text(description)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                VStack(alignment: .leading, spacing: 7) {
                    Text(isRecording ? "…" : displayText)
                        .font(.system(size: 15, weight: .semibold))
                        .lineLimit(1)
                        .minimumScaleFactor(0.85)
                    HStack(spacing: 6) {
                        Text(statusText)
                            .font(.system(size: 11, weight: .semibold))
                            .foregroundStyle(statusTone.foreground)
                            .padding(.horizontal, 8)
                            .padding(.vertical, 3)
                            .background(Capsule().fill(statusTone.background))
                        if isRecording {
                            Text(tr("hotkeys.recordingInline"))
                                .font(.system(size: 11, weight: .semibold))
                                .foregroundStyle(.blue)
                        }
                    }
                    HStack(spacing: 8) {
                        Button(customizeTitle, action: onCustomize)
                            .buttonStyle(.borderedProminent)
                            .controlSize(.small)
                        Button(restoreTitle, action: onRestore)
                            .buttonStyle(.bordered)
                            .controlSize(.small)
                            .disabled(!canRestore)
                    }
                }
                .frame(width: 230, alignment: .leading)
            }
        }
        .padding(12)
        .background(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .fill(Color(nsColor: .controlBackgroundColor))
        )
        .overlay(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .stroke(Color.secondary.opacity(0.14), lineWidth: 1)
        )
    }
}
private struct SettingsConfirmationView: View {
    let title: String
    let message: String
src/Apptag/ProAccessViews.swift
@@ -99,6 +99,8 @@
            return "pro.prompt.notes"
        case .persistentAppSorting:
            return "pro.prompt.sorting"
        case .customHotkeys:
            return "pro.prompt.customHotkeys"
        }
    }
}
src/Apptag/ProEntitlement.swift
@@ -8,6 +8,7 @@
    case layoutExport
    case unlimitedNotes
    case persistentAppSorting
    case customHotkeys
    var titleKey: String {
        switch self {
@@ -21,6 +22,8 @@
            return "pro.feature.notes"
        case .persistentAppSorting:
            return "pro.feature.sorting"
        case .customHotkeys:
            return "pro.feature.customHotkeys"
        }
    }
@@ -36,6 +39,8 @@
            return "pro.feature.notes.benefit"
        case .persistentAppSorting:
            return "pro.feature.sorting.benefit"
        case .customHotkeys:
            return "pro.feature.customHotkeys.benefit"
        }
    }
}
@@ -202,7 +207,7 @@
    static func isUnlocked(_ feature: ProFeature) -> Bool {
        switch feature {
        case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting:
        case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys:
            return accessState().isUnlocked
        }
    }
@@ -569,11 +574,16 @@
        originalAppVersion: String? = nil,
        originalPurchaseDate: Date? = nil
    ) {
        let previousState = accessState
        accessState = state
        ProEntitlementSnapshotStore.update { snapshot in
            snapshot.accessState = state
        }
        if previousState != state {
            NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged, object: nil)
        }
        if persistCache {
            writeCache(
                ProEntitlementCache(
src/Apptag/QuickSearch.swift
@@ -7,6 +7,7 @@
    static let tagLauncherQuickSearchDismissRequested = Notification.Name("TagLauncherQuickSearchDismissRequested")
    static let tagLauncherQuickSearchVisibilityChanged = Notification.Name("TagLauncherQuickSearchVisibilityChanged")
    static let tagLauncherHotkeyRegistrationChanged = Notification.Name("TagLauncherHotkeyRegistrationChanged")
    static let tagLauncherProEntitlementChanged = Notification.Name("TagLauncherProEntitlementChanged")
}
enum QuickSearchOpenSource {
@@ -82,6 +83,10 @@
        }
    }
    static var printableKeyCodes: Set<Int> {
        Set(keyCodeToPrintableScalar.keys)
    }
    private static let keyCodeToPrintableScalar: [Int: Character] = [
        kVK_ANSI_A: "A", kVK_ANSI_B: "B", kVK_ANSI_C: "C", kVK_ANSI_D: "D",
        kVK_ANSI_E: "E", kVK_ANSI_F: "F", kVK_ANSI_G: "G", kVK_ANSI_H: "H",
src/CHANGELOG.md
@@ -2,6 +2,18 @@
## [Unreleased]
## [8.2.0] — 2026-06-28
- 新增 Pro 自定义快捷键:Pro 用户可在“设置 > 快捷键”分别修改 App Grid 全局唤起和 Quick Search 全局唤起快捷键
- 免费用户继续固定使用默认快捷键:App Grid 为 `⌥⇧Space`,Quick Search 为 `Fn+Space`;即使本机存在历史自定义值,免费态也不会生效或写入真实自定义快捷键数据
- 自定义快捷键沿用 Carbon `RegisterEventHotKey`,不新增 Accessibility、Input Monitoring、CGEventTap 或私有 API;录制仅在设置窗口内通过本地键盘事件完成
- 快捷键保存改为事务式注册:候选快捷键先验证并注册成功后才替换旧快捷键和持久化;注册失败时旧快捷键继续可用
- 增加快捷键合法性校验:拒绝无修饰键、单普通键、Shift-only、Option-only、明显 macOS 保留组合,以及两个入口重复使用同一个快捷键
- Pro 权益变化后会重新注册有效快捷键;菜单栏和设置页统一显示当前真正生效的快捷键
- 补齐 29 个语种的自定义快捷键、录制、恢复默认、错误提示和 Pro 解锁文案
- 新增 `Scripts/pro_custom_hotkeys_qa.sh`,并扩展 Pro 门禁 QA,覆盖 Pro 硬门禁、事务式注册、递归安全的有效快捷键计算、本地录制和多语言文案
- 版本号更新为 `8.2.0`,Build 更新为 `20260628.2308`
## [8.1.7] — 2026-06-28
- 主题页新增可调的“暗色炫彩”和“亮色炫彩”主题:`深蓝`更名为`暗色炫彩`,`炫彩`更名为`亮色炫彩`,两者都纳入 Pro 主题范围并复用现有免费预览倒计时
src/Docs/Requirements/2026-06-28-pro-custom-hotkeys-prd.md
New file
@@ -0,0 +1,329 @@
# PRD - Pro 自定义快捷键
日期:2026-06-28
状态:需求口径已确认,待进入实现
目标版本:8.2.0
## 1. 背景
用户希望 TagLauncher 支持自定义快捷键。项目历史上曾做过类似能力,后续两次回滚:
- 3.1.13 曾新增全局快捷键设置,3.1.16 回滚。
- 7.5.0 曾新增 Quick Search 全局快捷键配置,7.5.1 回滚。
当前 App 只展示固定快捷键:
- App Grid 全局唤起:`⌥⇧Space`
- Quick Search 全局唤起:`Fn+Space`
- App Grid 内部 `Space` 打开 Quick Search:固定内部操作
本轮新产品约束:自定义快捷键必须作为 Pro 功能。普通用户不能自定义快捷键,但默认快捷键仍必须免费可用。
## 2. 目标
让 Pro 用户可以自定义两个全局唤起快捷键,同时保持免费用户基础体验完整。
完成标准:
- 免费用户继续可使用默认 App Grid 和 Quick Search 快捷键。
- 免费用户不能录制、保存或应用自定义快捷键。
- Pro 用户可以分别自定义 App Grid 全局唤起和 Quick Search 全局唤起。
- 自定义失败时旧快捷键继续可用,不出现快捷键丢失。
- 设置页、菜单栏、注册状态、提示文案保持一致。
- 29 种语言文案完整。
- macOS 14 / 15 / 26 完成回归。
- App Store 审核材料能清楚说明免费功能、Pro 功能和旧付费用户权益。
## 3. 不做范围
- 不自定义 App Grid 内部 `Space` 行为。
- 不把 Quick Search 基础能力变成 Pro。
- 不允许免费用户试用或预览自定义快捷键。
- 不允许通过外部授权、激活码、网站支付解锁。
- 不引入 Accessibility、Input Monitoring、CGEventTap 或私有 API。
- 首版不支持禁用某个全局快捷键。
- 首版不支持两个入口使用同一个快捷键。
- 首版不支持无修饰键、单字符键、modifier-only、Shift-only、Option-only 或明显系统保留组合。
## 4. 用户规则
### 4.1 免费用户
免费用户可以:
- 使用默认 App Grid 快捷键 `⌥⇧Space`。
- 使用默认 Quick Search 快捷键 `Fn+Space`。
- 在设置页查看当前快捷键和注册状态。
- 点击 Pro 入口了解“自定义快捷键”属于 Pro 功能。
- 恢复购买。
免费用户不可以:
- 进入真实快捷键录制状态。
- 保存自定义快捷键。
- 让自定义快捷键立即生效。
- 通过历史 defaults 或外部写入绕过 Pro 限制。
免费态下,即使本机存在历史自定义快捷键数据,有效快捷键也必须回到默认值。
### 4.2 Pro 用户
Pro 用户可以:
- 自定义 App Grid 全局唤起快捷键。
- 自定义 Quick Search 全局唤起快捷键。
- 单项恢复默认快捷键。
- 看到冲突、无效组合、注册失败提示。
- 重启后保留已保存的自定义快捷键。
Pro 用户遇到注册失败时:
- 新快捷键不保存。
- 旧快捷键继续可用。
- UI 显示明确失败原因或通用冲突提示。
### 4.3 旧付费用户
如果用户的 `originalAppVersion` 早于“免费 + Pro”的首个版本,则自动获得 Pro;必要时用 `originalPurchaseDate` 兜底。
旧付费用户自动获得 Pro 后,也自然获得自定义快捷键能力。
## 5. 设置页交互
位置:设置 > 快捷键。
显示内容:
- App Grid 全局唤起
- Quick Search 全局唤起
- App Grid 内部 `Space` 提示,仍作为固定说明展示,不提供自定义入口
- 触控板 URL 触发方式说明,保持现有逻辑
免费用户:
- 两个全局快捷键行展示当前默认值。
- 自定义入口显示 Pro 标识。
- 点击自定义入口时弹出 Pro 解锁提示。
- 不进入录制态,不写入任何快捷键数据。
Pro 用户:
- 点击“修改”进入录制态。
- `Esc` 取消录制。
- 录制无效组合时显示错误,不保存。
- 录制成功后即时生效。
- “恢复默认”只影响当前行。
## 6. 有效快捷键规则
有效快捷键由用户权益决定:
- 免费用户:始终使用默认快捷键。
- Pro 用户:优先使用已保存的合法自定义快捷键;没有保存值时使用默认快捷键。
- Pro 权益失效或不可用时:回退默认快捷键;历史自定义值可以保留为不可生效存档,重新获得 Pro 后再恢复生效。
## 7. 数据规则
必须满足:
- 免费用户不能写入真实自定义快捷键数据。
- 免费用户不能写入 pending、草稿、冲突状态等会被注册器误用的数据。
- 新数据结构不得被历史 `removeShortcutCustomizationDefaults()` 清理。
- 注册与保存必须事务式完成:先验证并注册候选快捷键,成功后再持久化;失败则不改旧值。
- 菜单栏、设置页、注册器必须从同一个有效快捷键来源读取。
## 8. App Store 审核风险
### 8.1 风险判断
本需求属于 App 内数字功能解锁,应通过 App Store In-App Purchase 的非消耗型 Pro 商品提供。这个方向与 Apple 对 App 内功能解锁使用 IAP 的要求一致。
主要风险不在“自定义快捷键作为 Pro”本身,而在以下几点:
- 免费版如果不可用,可能被认为下载后基础功能不足。
- 如果使用外部支付或自有授权,会违反 IAP 规则。
- 如果为了快捷键引入 Accessibility、Input Monitoring 或键盘监听权限,会增加审核解释成本。
- 如果元数据没有解释旧付费用户自动获得 Pro,用户和审核员可能误解为重复收费。
### 8.2 降风险口径
提交审核时必须说明:
- App 现在免费下载。
- 免费用户仍可使用默认快捷键、标签整理、应用启动和基础 Quick Search。
- Pro 为一次性买断,不是订阅。
- Pro 解锁自定义快捷键等高级能力。
- 旧付费下载用户会自动获得终身 Pro,不需要再次购买。
- 自定义快捷键不需要 Accessibility、Input Monitoring 或额外隐私权限。
- 快捷键注册失败会保留旧快捷键,不影响基础使用。
### 8.3 官方参考
- Apple App Review Guidelines:https://developer.apple.com/app-store/review/guidelines/
- App Store Connect - Configure in-app purchases:https://developer.apple.com/help/app-store-connect/configure-in-app-purchase-settings/overview-for-configuring-in-app-purchases/
## 9. 多语言要求
29 种语言必须新增或校对以下文案:
- 自定义快捷键
- App Grid 快捷键
- Quick Search 快捷键
- 修改
- 正在录制
- 按下新的快捷键
- 取消
- 恢复默认
- 此快捷键不可用
- 此快捷键已被占用
- 此组合不支持
- 需要 Pro
- 解锁 Pro 后可自定义快捷键
- 默认快捷键仍可免费使用
快捷键符号建议统一保留 `⌘`、`⌥`、`⇧`、`⌃`,避免逐语种翻译符号本身。`Space`、`Fn` 需要本地化显示或统一符号策略。
## 10. QA 验收用例
### HKEY-01 免费默认可用
免费用户启动 App 后:
- `⌥⇧Space` 可以打开 App Grid。
- `Fn+Space` 可以打开 Quick Search。
- 设置页显示两个默认快捷键。
### HKEY-02 免费用户不能自定义
免费用户点击自定义入口:
- 出现 Pro 解锁提示。
- 不进入真实录制态。
- 不写入自定义快捷键数据。
- 默认快捷键仍继续可用。
### HKEY-03 Pro 用户可自定义 App Grid
Pro 用户修改 App Grid 快捷键后:
- 新快捷键打开 App Grid。
- 旧快捷键不再触发 App Grid。
- 设置页和菜单栏显示新快捷键。
- 重启后仍生效。
### HKEY-04 Pro 用户可自定义 Quick Search
Pro 用户修改 Quick Search 快捷键后:
- 新快捷键打开 Quick Search。
- 触发后可以立即输入。
- 旧快捷键不再触发 Quick Search。
- 重启后仍生效。
### HKEY-05 冲突失败回滚
当候选快捷键被系统或其他 App 占用:
- 显示失败提示。
- 不保存候选快捷键。
- 旧快捷键继续可用。
### HKEY-06 无效组合拒绝
以下组合不得保存:
- 空快捷键
- 单普通键
- modifier-only
- Shift-only
- Option-only
- 两个入口重复组合
- 明显系统保留组合
### HKEY-07 恢复默认
Pro 用户点击恢复默认:
- 当前行恢复到默认快捷键。
- 另一个快捷键不受影响。
- 重启后仍为默认值。
### HKEY-08 权益状态变化
当 Pro 状态变化:
- 获得 Pro 后可以自定义。
- 失去 Pro 后有效快捷键回到默认。
- 重新获得 Pro 后,可恢复之前保存的自定义值。
### HKEY-09 历史数据清理
存在旧版本快捷键 defaults 时:
- 不应误用历史不可控数据。
- 不应清理新 schema 的有效数据。
- 免费用户仍使用默认快捷键。
### HKEY-10 多系统回归
必须覆盖:
- macOS 14 真机
- macOS 15
- macOS 26
- 全屏
- Split View
- 多显示器
- 隐藏 Dock
- 菜单栏入口 fallback
- 连续快速触发
## 11. 提交市场前检查
- App 价格为免费。
- Pro IAP 为非消耗型一次性买断。
- App 与 IAP 状态可一并提交审核。
- Review Notes 写清楚免费功能、Pro 功能、旧用户自动 Pro 和快捷键无额外权限。
- 元数据、截图、支持页不残留“付费下载后才能用”的旧口径。
- 29 语种 Pro / 快捷键文案完整。
- TestFlight 或本地 QA 记录覆盖免费、Pro、老付费自动 Pro。
## 12. 团队讨论结论
产品总监:
- 推荐把自定义快捷键纳入同一个 Lifetime Pro Unlock。
- 免费用户保留默认快捷键,不做免费预览。
- Pro 只解锁“自定义”能力,不锁基础启动和基础 Quick Search。
架构师:
- 继续走 Carbon `RegisterEventHotKey`。
- 不引入 Accessibility、Input Monitoring、CGEventTap。
- 有效快捷键必须由单一来源统一提供。
- 保存必须事务式,失败回滚。
代码审核员:
- 历史回滚风险来自 OS 注册状态、持久化状态、UI 显示状态不一致。
- 必须处理历史快捷键 defaults 清理逻辑。
- Pro gating 不能只做 UI,保存和有效值读取也必须拦截。
QA:
- 必须覆盖免费、Pro、老付费自动 Pro 三种状态。
- 必须覆盖 macOS 14 / 15 / 26。
- 必须补冲突失败回滚和重启持久化。
- 多语言文案必须完整校对。
## 13. 需要用户确认的默认口径
用户已于 2026-06-28 确认以下口径全部无异议,后续实现按此执行:
1. 自定义快捷键作为现有 Lifetime Pro Unlock 的一部分,不新增单独 IAP。
2. 免费用户显示锁定入口,点击后弹 Pro 提示;不隐藏整项。
3. 首版不允许禁用某个全局快捷键。
4. 首版不允许两个入口使用同一个快捷键。
5. 首版禁止无修饰键、modifier-only、Shift-only、Option-only 和明显系统保留组合。
6. 默认快捷键继续保持 `⌥⇧Space` 和 `Fn+Space`,本需求不顺手改默认值。
7. 历史旧快捷键 defaults 不迁移为有效设置;只清理或忽略,不服务少量历史测试用户。
src/Docs/Requirements/2026-06-28-pro-custom-hotkeys-todo.md
New file
@@ -0,0 +1,55 @@
# 8.2.0 Pro 自定义快捷键 TODO
目标:让 Pro 用户可以自定义全局快捷键;免费用户继续固定使用默认快捷键,不允许保存或启用自定义快捷键。
## 版本与范围
- 版本号:8.2.0
- 保持默认快捷键不变:
  - 显示应用列表:Option + Shift + Space
  - 全局 Quick Search:Fn + Space
- 不做原生三指/四指捕获。
- 不引入 Accessibility、Input Monitoring、CGEventTap 或私有 API。
- 不迁移旧实验快捷键 key;旧 key 继续由 `AppDefaults.removeShortcutCustomizationDefaults()` 清理。
## 实现 TODO
- [x] 增加 `ProFeature.customHotkeys`,把自定义快捷键纳入 Pro 权益体系。
- [x] 增加统一的快捷键存储与校验层,使用新命名空间 `customHotkeys.v1.*`。
- [x] App 启动和菜单显示统一读取“有效快捷键”:
  - 免费用户:始终默认快捷键。
  - Pro 用户:合法自定义快捷键优先,否则默认快捷键。
- [x] 注册快捷键时采用事务式流程:候选快捷键先注册成功,再替换旧引用并保存。
- [x] 设置页快捷键页签增加自定义入口、恢复默认入口、录制状态与错误提示。
- [x] Pro 状态变化时重新注册有效快捷键。
- [x] 更新 29 语种本地化文案。
## QA TODO
- [x] 静态检查 Pro gate:UI、保存、注册有效快捷键均不能绕过 Pro。
- [x] 静态检查 Carbon-only:不得出现 CGEventTap、Accessibility、Input Monitoring 等路径。
- [x] 静态检查默认快捷键:免费状态始终回退默认值。
- [x] 静态检查事务式注册:失败时旧快捷键不被先卸载。
- [x] 29 语种本地化完整性检查。
- [x] macOS 14 availability/typecheck。
- [x] build.sh 构建与版本元数据检查。
## Go / No-Go
- Go:源码、QA、构建、版本号和 changelog 全部通过后冻结。
- No-Go:任何 Pro gate 可绕过、快捷键注册失败导致默认快捷键不可用、或引入额外系统权限。
## 完成记录
- 版本:8.2.0
- Build:20260628.2308
- QA:
  - `Scripts/pro_custom_hotkeys_qa.sh`
  - `Scripts/pro_feature_gate_qa.sh`
  - `Scripts/pro_localization_qa.sh`
  - `Scripts/macos14_availability_typecheck_qa.sh`
  - `Scripts/macos14_build_metadata_qa.sh`
- 构建产物:
  - `build/TagLauncher.app`
  - `build/TagLauncher.dmg`
- DMG SHA256:`493b77349285f0254935ffa10feefbc278ad18b5f91c12fca5579d6599810bea`
src/Release/AppStore-8.2.0-20260628.2308/Archive/SHA256SUMS.txt
New file
@@ -0,0 +1 @@
493b77349285f0254935ffa10feefbc278ad18b5f91c12fca5579d6599810bea  src/Release/AppStore-8.2.0-20260628.2308/Archive/TagLauncher-8.2.0-build20260628.2308.dmg
src/Release/AppStore-8.2.0-20260628.2308/Archive/TagLauncher-8.2.0-build20260628.2308.dmg
Binary files differ
src/Release/AppStore-8.2.0-20260628.2308/QA/QA-Report.md
New file
@@ -0,0 +1,45 @@
# QA Report - TagLauncher 8.2.0 (20260628.2308)
## Scope
- Feature: Pro-only custom global hotkeys.
- Version: 8.2.0
- Build: 20260628.2308
- Package: `Archive/TagLauncher-8.2.0-build20260628.2308.dmg`
## Passed Checks
- `Scripts/pro_custom_hotkeys_qa.sh`
  - Pro gate for save/restore paths.
  - Free users always fall back to default effective hotkeys.
  - Carbon `RegisterEventHotKey` remains the global hotkey backend.
  - No CGEventTap, Accessibility, Input Monitoring, or private gesture APIs introduced.
  - Transactional registration preserves old hotkeys on candidate failure.
  - Duplicate-safe effective hotkey calculation avoids recursive validation.
  - 29 localization files include the custom hotkey keys.
- `Scripts/pro_feature_gate_qa.sh`
  - Pro entitlement, StoreKit, legacy unlock, premium theme, layout import/export, note quota, sorting preview, and custom hotkey gates are wired.
- `Scripts/pro_localization_qa.sh`
  - 29 languages, 49 `pro.*` keys, no placeholder artifacts or English fallback copies.
- `Scripts/macos14_availability_typecheck_qa.sh`
  - macOS 14 arm64 typecheck passed.
- `Scripts/macos14_build_metadata_qa.sh`
  - `LSMinimumSystemVersion=14.0`, binary minos `14.0`, arch `arm64`.
- `build.sh`
  - App bundle built successfully.
- `make_dmg.sh`
  - DMG created successfully.
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`
  - App signature verified on disk.
- `hdiutil verify build/TagLauncher.dmg`
  - DMG checksum valid.
## Artifact Checksum
```text
493b77349285f0254935ffa10feefbc278ad18b5f91c12fca5579d6599810bea  TagLauncher-8.2.0-build20260628.2308.dmg
```
## Residual Risk
- GUI-level manual validation is still recommended for actual key recording UX because static QA cannot press arbitrary global key combinations in the user's environment without risking conflicts with local apps or input methods.
src/Release/AppStore-8.2.0-20260628.2308/RELEASE_MANIFEST.md
New file
@@ -0,0 +1,27 @@
# Release Manifest - TagLauncher 8.2.0 (20260628.2308)
## Build
- Version: 8.2.0
- Build: 20260628.2308
- Frozen from branch: `codex/pro-custom-hotkeys-8.2.0`
## Artifacts
- DMG: `Archive/TagLauncher-8.2.0-build20260628.2308.dmg`
- SHA256:
```text
493b77349285f0254935ffa10feefbc278ad18b5f91c12fca5579d6599810bea
```
## QA Evidence
- `QA/QA-Report.md`
## Changelog Summary
- Pro users can customize the App Grid global hotkey and the global Quick Search hotkey.
- Free users keep the default hotkeys and cannot save or enable custom hotkeys.
- Registration is transactional, so a failed candidate does not remove the previous working shortcut.
- No Accessibility, Input Monitoring, CGEventTap, or private API path was introduced.
src/Scripts/pro_custom_hotkeys_qa.sh
New file
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT_DIR" <<'PY'
import json
import re
import sys
from pathlib import Path
root = Path(sys.argv[1])
app_dir = root / "Apptag"
localization_dir = app_dir / "Localization"
EXPECTED_LANGUAGE_COUNT = 29
REQUIRED_HOTKEY_KEYS = [
    "hotkeys.customize",
    "hotkeys.restoreDefault",
    "hotkeys.recording",
    "hotkeys.recordingInline",
    "hotkeys.recordingCancelled",
    "hotkeys.saved",
    "hotkeys.restored",
    "hotkeys.proRequired",
    "hotkeys.error.modifierRequired",
    "hotkeys.error.unsupportedKey",
    "hotkeys.error.reserved",
    "hotkeys.error.duplicate",
    "hotkeys.error.registrationFailed",
    "pro.feature.customHotkeys",
    "pro.feature.customHotkeys.benefit",
    "pro.prompt.customHotkeys",
]
def fail(message: str) -> None:
    raise SystemExit(f"FAIL: {message}")
def read(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except Exception as error:
        fail(f"could not read {path}: {error}")
def require(source: str, needle: str, message: str) -> None:
    if needle not in source:
        fail(message)
def function_body(source: str, signature: str) -> str:
    start = source.find(signature)
    if start == -1:
        fail(f"missing function signature: {signature}")
    brace = source.find("{", start)
    if brace == -1:
        fail(f"missing function body for: {signature}")
    depth = 0
    for index in range(brace, len(source)):
        char = source[index]
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return source[brace + 1:index]
    fail(f"unterminated function body for: {signature}")
def load_json(path: Path) -> dict[str, str]:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception as error:
        fail(f"{path.name} is not valid JSON: {error}")
    if not isinstance(data, dict):
        fail(f"{path.name} must contain a JSON object")
    return data
settings = read(app_dir / "LauncherHotkeySettings.swift")
app = read(app_dir / "ApptagApp.swift")
preferences = read(app_dir / "PreferencesView.swift")
pro = read(app_dir / "ProEntitlement.swift")
access_views = read(app_dir / "ProAccessViews.swift")
defaults = read(app_dir / "AppDefaults.swift")
quick_search = read(app_dir / "QuickSearch.swift")
require(settings, 'private static let namespace = "customHotkeys.v1"', "custom hotkeys must use the new v1 namespace")
require(settings, "defaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey", "default hotkey helper is missing")
require(settings, "kind.hotkey", "default hotkey must remain the fixed built-in shortcut")
require(settings, "ProEntitlementPolicy.isUnlocked(.customHotkeys)", "effective hotkey must be gated by Pro entitlement")
require(settings, "structuralValidationError(for: saved) == nil", "stored hotkeys must pass structural validation before use")
require(settings, "!duplicatesOtherEffectiveHotkey(saved, kind: kind)", "effective hotkey must reject duplicated saved shortcuts")
require(settings, "effectiveHotkeyIgnoringDuplicateCheck(for:", "duplicate validation must avoid recursive effectiveHotkey calls")
require(settings, "kEventKeyModifierFnMask", "Fn modifier support is required for the default Quick Search hotkey")
require(settings, "modifiers == UInt32(kEventKeyModifierFnMask)", "Fn-only must be restricted explicitly")
require(settings, "hotkey.keyCode == UInt32(kVK_Space)", "Fn-only must only be valid for Space")
require(settings, "isReservedBySystem", "system-reserved shortcuts must be rejected")
effective_body = function_body(settings, "static func effectiveHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkey")
if "validationError(for:" in effective_body:
    fail("effectiveHotkey must not call validationError because duplicate checks can recurse")
validation_body = function_body(settings, "static func validationError(for hotkey: LauncherHotkey, kind: LauncherHotkeyKind) -> LauncherHotkeyValidationError?")
require(validation_body, "structuralValidationError(for:", "public validation must run structural checks")
require(validation_body, "duplicatesOtherEffectiveHotkey", "public validation must reject duplicates")
apply_body = function_body(app, "func applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult")
require(apply_body, "guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else", "AppDelegate save path must be Pro-gated")
require(apply_body, "LauncherHotkeySettings.validationError(for: hotkey, kind: kind)", "AppDelegate save path must validate the candidate")
require(apply_body, "activateHotkey(hotkey, for: kind, persistCustom: true)", "custom save must go through transactional activation")
restore_body = function_body(app, "func restoreDefaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult")
require(restore_body, "guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else", "restore default path must be Pro-gated")
require(restore_body, "persistCustom: false", "restore default must clear custom storage after successful registration")
activate_body = function_body(app, "private func activateHotkey(")
try_register_index = activate_body.find("tryRegisterHotkey(hotkey")
success_index = activate_body.find("case .success(let newRef):")
if try_register_index == -1 or success_index == -1:
    fail("transactional registration body is incomplete")
success_body = activate_body[success_index:]
unregister_index = success_body.find("UnregisterEventHotKey(oldRef)")
save_index = success_body.find("LauncherHotkeySettings.save")
if unregister_index == -1 or save_index == -1:
    fail("transactional success branch is incomplete")
if not (try_register_index < success_index and unregister_index < save_index):
    fail("custom hotkey must register first, then unregister old ref, then persist")
require(activate_body, "return .registrationFailed(status)", "registration failure must not save the candidate")
require(app, "observeProEntitlementChanges()", "AppDelegate must observe Pro entitlement changes")
require(app, "registerConfiguredHotkeys()", "Pro entitlement changes must re-register effective hotkeys")
require(app, "LauncherHotkeySettings.effectiveHotkey(for: .main).displayString", "menu title must display the effective App Grid shortcut")
require(app, "LauncherHotkeySettings.effectiveHotkey(for: kind)", "hotkey registration must read effective shortcuts")
require(app, "RegisterEventHotKey", "Carbon RegisterEventHotKey must remain the global hotkey backend")
require(app, "UnregisterEventHotKey(oldRef)", "old hotkey ref must be released after successful replacement")
for forbidden in ["CGEventTapCreate", "AXIsProcessTrusted", "IOHIDManagerCreate", "addGlobalMonitorForEvents(matching: .keyDown"]:
    if forbidden in app + preferences + settings + quick_search:
        fail(f"forbidden API used for custom hotkeys: {forbidden}")
require(preferences, "presentSettingsProPrompt(for: .customHotkeys)", "free users must see the Pro prompt instead of recording")
require(preferences, "NSEvent.addLocalMonitorForEvents(matching: .keyDown)", "recording must use local key-down capture in Settings")
require(preferences, "Int(event.keyCode) == kVK_Escape", "Esc must cancel recording")
require(preferences, "LauncherHotkeySettings.candidate(from: event)", "recording must normalize NSEvent into LauncherHotkey")
require(preferences, ".applyCustomHotkey(candidate, for: kind)", "recording must call the AppDelegate transactional save path")
require(preferences, "CustomizableHotkeyInfoRow", "Settings hotkey rows must expose custom controls")
require(preferences, "StaticHotkeyInfoRow(", "internal App Grid Space shortcut must remain static")
require(preferences, "LauncherHotkeySettings.effectiveHotkey(for: .main).displayString", "Settings must display effective App Grid shortcut")
require(preferences, "LauncherHotkeySettings.effectiveHotkey(for: .quickSearch).displayString", "Settings must display effective Quick Search shortcut")
require(pro, "case customHotkeys", "ProFeature.customHotkeys is missing")
require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "customHotkeys must be unlocked only by the Pro access state")
require(pro, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "entitlement changes must notify hotkey registration")
require(access_views, "case .customHotkeys:", "Pro prompt routing must include custom hotkeys")
cleanup_body = function_body(defaults, "private static func removeShortcutCustomizationDefaults()")
if "customHotkeys.v1" in cleanup_body:
    fail("legacy cleanup must not delete new custom hotkey storage")
language_files = sorted(localization_dir.glob("*.json"))
if len(language_files) != EXPECTED_LANGUAGE_COUNT:
    fail(f"expected {EXPECTED_LANGUAGE_COUNT} localization files, found {len(language_files)}")
for path in language_files:
    data = load_json(path)
    missing = [
        key for key in REQUIRED_HOTKEY_KEYS
        if not isinstance(data.get(key), str) or not data[key].strip()
    ]
    if missing:
        fail(f"{path.name} missing custom hotkey translations: {', '.join(missing)}")
    if "%status%" not in data["hotkeys.error.registrationFailed"]:
        fail(f"{path.name} hotkeys.error.registrationFailed must keep %status% placeholder")
print("PASS Pro custom hotkeys QA: Pro gating, transactional Carbon registration, duplicate-safe effective shortcuts, local recording, and 29-language copy are wired")
PY
src/Scripts/pro_feature_gate_qa.sh
@@ -62,6 +62,7 @@
    "layoutExport",
    "unlimitedNotes",
    "persistentAppSorting",
    "customHotkeys",
]:
    require(pro, f"case {case}", f"missing ProFeature.{case}")
@@ -80,12 +81,18 @@
require(pro, "ProEntitlementSnapshotStore", "sync entitlement snapshot for data-layer policy is missing")
require(pro, "manualNonEmptyNoteCount", "note quota must count existing manual notes")
require(pro, "manualNonEmptyNotePaths", "note quota must avoid double-counting existing notes")
require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "custom hotkeys must be unlocked only by the Pro access state")
require(pro, "NotificationCenter.default.post(name: .tagLauncherProEntitlementChanged", "Pro entitlement changes must notify hotkey registration")
effective_theme = function_body(pro, "static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme")
require(effective_theme, "return .defaultLight", "locked premium theme must fall back to defaultLight")
require(effective_theme, "canUseTheme(storedTheme)", "effectiveTheme must check Pro theme entitlement")
require(app, "ProEntitlementCenter.shared.start()", "App startup does not start Pro entitlement center")
require(app, "observeProEntitlementChanges()", "App startup must observe Pro entitlement changes for custom hotkeys")
require(app, "applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind)", "AppDelegate custom hotkey save entry is missing")
require(app, "guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else", "custom hotkey save/restore must be hard-gated by Pro")
require(app, "LauncherHotkeySettings.effectiveHotkey(for: kind)", "global hotkey registration must use the entitlement-aware effective shortcut")
require(theme, "var requiresPro: Bool", "AppGridTheme does not expose Pro requirement")
require(theme, "ProEntitlementConfig.freeThemes.contains(self)", "theme Pro requirement must use central free theme set")
@@ -126,6 +133,8 @@
require(preferences, "ProStatusPill(text: tr(\"pro.card.badge\"), style: .locked, compact: true)", "locked data actions must show compact Pro pill")
require(preferences, "presentSettingsProPrompt(for: .premiumThemes)", "premium theme selection prompt is missing")
require(preferences, "presentSettingsProPrompt(for: .customHotkeys)", "custom hotkey Pro prompt is missing")
require(preferences, "NSEvent.addLocalMonitorForEvents(matching: .keyDown)", "custom hotkey recording must be local to Settings")
require(preferences, "proEntitlement.startThemePreview(for: theme, tuning:", "premium theme free preview is missing")
require(preferences, "proEntitlement.stopThemePreview()", "theme preview cleanup is missing")
require(pro, "func remainingSeconds(at date: Date = Date()) -> Int", "theme preview remaining-seconds helper is missing")