Ariver
2026-05-17 e1226e6ae5a75388fe5d1a0e1ed008f80fee25ed
Release Apptag 6.0.5 build 610
37 files modified
1712 ■■■■■ changed files
AppStore_Submission.md 51 ●●●●● patch | view | raw | blame | history
Apptag/AppGridItem.swift 103 ●●●●● patch | view | raw | blame | history
Apptag/ApptagApp.swift 15 ●●●●● patch | view | raw | blame | history
Apptag/ContentView.swift 593 ●●●●● patch | view | raw | blame | history
Apptag/DataLayer.swift 273 ●●●●● patch | view | raw | blame | history
Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
Apptag/Localization/ar-Najdi.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ar.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/cs.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/da.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/de.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/en.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/es.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/fr.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/id.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/it.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ja.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ko.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ms.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/nb.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/nl.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/nn.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/no.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/pl.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/pt-BR.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ro.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ru.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/sr-Cyrl.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/sv.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/th.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/tr.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/uk.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/vi.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/zh-Hans.json 22 ●●●●● patch | view | raw | blame | history
Apptag/Localization/zh-Hant.json 22 ●●●●● patch | view | raw | blame | history
Apptag/TagGroupView.swift 4 ●●●● patch | view | raw | blame | history
CHANGELOG.md 31 ●●●●● patch | view | raw | blame | history
AppStore_Submission.md
@@ -1,6 +1,6 @@
# Apptag — Mac App Store 提审资料
> 版本: 5.7.8 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-15
> 版本: 6.0.5 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-17
---
@@ -11,8 +11,8 @@
| **App 名称** | Apptag |
| **副标题** | Tag-Based App Launcher |
| **Bundle ID** | com.apptag.launcher |
| **版本号** | 5.7.8 |
| **Build 号** | 583 |
| **版本号** | 6.0.5 |
| **Build 号** | 610 |
| **SKU** | apptag-mac-001 |
| **主要类别** | Utilities (工具) |
| **次要类别** | Productivity (效率) |
@@ -187,36 +187,23 @@
## 8. What's New (本次版本更新说明)
```
Apptag 5.7.8
Apptag 6.0.5
- Fixed duplicate overlay keyboard event monitors that could accumulate after repeatedly opening TagLauncher
- Removed obsolete tag reorder fallback handlers that could interfere with click handling after drag sorting
- Reduced unnecessary Dock refreshes when unrelated preferences change
- Made the menu bar icon mandatory and removed the Show Menu Bar Icon preference across all localizations
- Removed unused macOS app-menu items and added a localized Show App List command with symbol-only shortcut glyphs
- Replaced the default macOS Services/Hide menu groups and made the menu bar status item use a fixed visible icon slot
- Replaced the menu bar SF Symbol with a code-drawn template icon so the status item remains visible even if symbol rendering fails
- Improved the General preferences layout for long localized text by using two-column style choices and aligned setting rows
- Refined the app-list style chooser so Flat spans the first row and the remaining four styles form a balanced 2x2 grid
- Reduced the app-list style option height and added compact visual glyphs for flat, masonry, and grid layouts
- Added a symbol-only shortcut hint on the Language, Data, and About preference tabs for Option + Shift + Space
- Fixed the menu bar icon visibility issue
- Fixed Preferences placement when opened above TagLauncher from a fullscreen app Space
- Fixed tag navigation reorder mode so clicking elsewhere exits the shaking state
- Expanded localization support to 29 language variants
- Updated the Language preferences tab to a two-column layout for better readability
- Added long-press drag sorting for tag navigation in normal app-list views
- Tag sorting now works from top, left, or right tag navigation positions across all 5 app-list styles
- Tag reorder animations now update both the tag navigation and matching app containers
- Added automatic migration for legacy non-sandbox tag databases into the App Store sandbox container
- Added a dedicated Language tab in Preferences with live language switching
- Preferences now opens centered above the active TagLauncher overlay, including multi-display setups
- Improved tag reordering in edit mode with live visual feedback and synchronized group layout updates
- Renamed backup actions to Export/Import Categories & Layout across all supported languages
- Added Mac App Store sandbox entitlements and App Store build validation
- Hid Launch at Login controls in sandboxed App Store builds
- Removed deprecated app launch API usage
- Version kept at 5.7.8, Build updated to 583
- Added an Uncommon app marker for apps you are still learning, with optional hover notes
- New apps can be marked as Uncommon automatically and are cleared after enough launches from TagLauncher
- Added local note editing for Uncommon apps; notes stay in the exported JSON even if the marker is removed later
- Added an Uncommon bubble toggle in the Data preferences tab, enabled by default
- Reworked batch category editing into clear Add Tags and Remove Tags modes
- Fixed batch editing so selected apps no longer inherit unrelated tags from each other
- Added clear confirmation feedback showing the real number of tag links added or removed
- Improved Remove Tags mode with red delete indicators and safer label truncation
- Fixed edit-sidebar layout so long localized hints no longer stretch the page
- Removed the edit-page center title and gave more room to the action hint and confirmation button
- Improved shortcut hints in Language, Data, and About settings pages using symbol-only key graphics
- Fixed a crash caused by toolbar hint layout constraints in the edit screen
- Fixed Safari and other modern macOS system apps being missed when they live behind Cryptex system paths
- Kept duplicate app icons out by resolving symlinks for scanning deduplication
- Version updated to 6.0.5, Build updated to 610
```
---
Apptag/AppGridItem.swift
@@ -10,10 +10,14 @@
    var sourceTag: String? = nil
    var dragModeActive: Bool = false
    var onDragModeChange: ((Bool) -> Void)? = nil
    var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil
    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
    let onSelect: () -> Void
    @State private var isHovered = false
    @State private var wiggle = false
    @State private var globalFrame: CGRect = .zero
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = true
    static let hoverScale: CGFloat = 1.22
    static let labelHeight: CGFloat = 14
@@ -62,9 +66,28 @@
        .padding(.vertical, 8)
        .padding(.horizontal, 4)
        .frame(width: Self.stableWidth(iconSize: iconSize), height: Self.stableHeight(iconSize: iconSize))
        .background(
            GeometryReader { proxy in
                Color.clear
                    .onAppear { globalFrame = proxy.frame(in: .global) }
                    .onChange(of: proxy.frame(in: .global)) { _, newFrame in
                        globalFrame = newFrame
                    }
            }
        )
        .contentShape(Rectangle())
        .onHover { hovering in
            isHovered = hovering
            if showUncommonAppBubbles && app.isUncommon {
                onBubbleHover?(app, globalFrame, hovering)
            }
        }
        .contextMenu {
            if app.isUncommon {
                Button(tr("appNote.edit")) {
                    onEditNote?(app, globalFrame)
                }
            }
        }
        .rotationEffect(.degrees(dragModeActive ? (wiggle ? 2.0 : -2.0) : 0))
        .animation(
@@ -79,6 +102,86 @@
    }
}
enum BubblePlacement {
    case above
    case below
}
struct AppNameBubble: View {
    let appName: String
    let note: String?
    let isEditing: Bool
    let placement: BubblePlacement
    @Binding var draftNote: String
    var noteFocused: FocusState<Bool>.Binding
    let onCommit: () -> Void
    let onCancel: () -> Void
    private var displayNote: String {
        note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    }
    var body: some View {
        content
        .onExitCommand(perform: onCancel)
    }
    private var content: some View {
        VStack(spacing: isEditing || !displayNote.isEmpty ? 8 : 0) {
            Text(appName)
                .font(.system(size: isEditing ? 22 : 24, weight: .bold))
                .foregroundStyle(.white)
                .lineLimit(1)
                .truncationMode(.middle)
                .frame(maxWidth: .infinity)
            if isEditing {
                VStack(alignment: .trailing, spacing: 4) {
                    TextField(tr("appNote.placeholder"), text: limitedDraft)
                        .textFieldStyle(.plain)
                        .font(.system(size: 15, weight: .medium))
                        .foregroundStyle(.white)
                        .padding(.horizontal, 12)
                        .frame(height: 36)
                        .background(
                            RoundedRectangle(cornerRadius: 9)
                                .fill(Color.white.opacity(0.12))
                        )
                        .focused(noteFocused)
                        .onSubmit(onCommit)
                    Text("\(draftNote.count) / \(TagDatabase.maxAppNoteLength)")
                        .font(.system(size: 10, weight: .medium))
                        .foregroundStyle(.white.opacity(0.35))
                }
            } else if !displayNote.isEmpty {
                Text(displayNote)
                    .font(.system(size: 14, weight: .medium))
                    .lineSpacing(2)
                    .foregroundStyle(.white.opacity(0.78))
                    .lineLimit(4)
                    .multilineTextAlignment(.center)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity)
            }
        }
        .padding(.horizontal, isEditing ? 22 : 24)
        .padding(.vertical, isEditing ? 18 : 16)
        .background(
            RoundedRectangle(cornerRadius: 18, style: .continuous)
                .fill(Color.black.opacity(0.92))
                .shadow(color: .black.opacity(0.34), radius: 24, y: 16)
                .shadow(color: .black.opacity(0.18), radius: 8, y: 3)
        )
    }
    private var limitedDraft: Binding<String> {
        Binding(
            get: { draftNote },
            set: { draftNote = String($0.prefix(TagDatabase.maxAppNoteLength)) }
        )
    }
}
private struct DraggableAppIconView: NSViewRepresentable {
    let icon: NSImage
    let iconSize: CGFloat
Apptag/ApptagApp.swift
@@ -796,6 +796,7 @@
    @AppStorage("hideAppNames") private var hideAppNames = false
    @AppStorage("showDockIcon") private var showDockIcon = false
    @AppStorage("launchAtLogin") private var launchAtLogin = true
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = true
    @State private var selectedLanguage = L10n.currentCode
    @State private var isRefreshingLanguage = false
    @State private var allApps: [AppInfo] = []
@@ -1064,7 +1065,19 @@
            // Tab 4: Data
            VStack(spacing: 0) {
                Spacer(minLength: 96)
                Spacer(minLength: 54)
                VStack(alignment: .leading, spacing: 5) {
                    Toggle(tr("settings.uncommonBubble"), isOn: $showUncommonAppBubbles)
                        .font(.system(size: 13, weight: .medium))
                    Text(tr("settings.uncommonBubbleDesc"))
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
                .frame(width: 420, alignment: .leading)
                Spacer(minLength: 28)
                VStack(alignment: .leading, spacing: 8) {
                    Text(tr("settings.backup"))
Apptag/ContentView.swift
@@ -16,6 +16,11 @@
    case editingApps
}
enum EditTagOperation {
    case add
    case remove
}
// MARK: - Native NSTextField (avoids SwiftUI TextField event issues)
/// Custom container that wraps NSTextField so that hitTest returns the container
@@ -220,6 +225,64 @@
// MARK: - Full-Screen Overlay
private struct AppBubbleContext {
    let app: AppInfo
    let frame: CGRect
}
private struct EditActionFeedback: Identifiable {
    let id = UUID()
    let title: String
    let message: String
}
private struct EditActionFeedbackBubble: View {
    let title: String
    let message: String
    let onClose: () -> Void
    var body: some View {
        VStack(alignment: .leading, spacing: 14) {
            HStack(alignment: .top, spacing: 12) {
                Text(title)
                    .font(.system(size: 24, weight: .bold))
                    .foregroundStyle(.white)
                    .frame(maxWidth: .infinity, alignment: .leading)
                Button(action: onClose) {
                    Image(systemName: "xmark")
                        .font(.system(size: 12, weight: .bold))
                        .foregroundStyle(.white.opacity(0.82))
                        .frame(width: 26, height: 26)
                        .background(
                            Circle()
                                .fill(Color.white.opacity(0.12))
                        )
                }
                .buttonStyle(.plain)
            }
            ScrollView(.vertical, showsIndicators: true) {
                Text(message)
                    .font(.system(size: 15, weight: .medium))
                    .lineSpacing(3)
                    .foregroundStyle(.white.opacity(0.84))
                    .multilineTextAlignment(.leading)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
            .frame(maxHeight: 220)
        }
        .padding(.horizontal, 24)
        .padding(.vertical, 20)
        .background(
            RoundedRectangle(cornerRadius: 18, style: .continuous)
                .fill(Color.black.opacity(0.92))
                .shadow(color: .black.opacity(0.34), radius: 24, y: 16)
                .shadow(color: .black.opacity(0.18), radius: 8, y: 3)
        )
    }
}
struct ContentView: View {
    let hideOverlay: () -> Void
@@ -231,7 +294,9 @@
    @State private var editPhase: EditPhase = .none
    @State private var selectedAppPaths: Set<URL> = []
    @State private var selectedTagNames: Set<String> = []
    @State private var successToast: String? = nil
    @State private var removableTagNames: Set<String> = []
    @State private var editTagOperation: EditTagOperation = .add
    @State private var editActionFeedback: EditActionFeedback? = nil
    @State private var draggedTagNames: [String] = []  // live drag order
    @State private var dragItem: String? = nil          // currently dragged tag
    @State private var tagReorderFrames: [String: CGRect] = [:]
@@ -246,6 +311,10 @@
    @State private var dropRefreshVisible = false
    @State private var dropRefreshStartedAt: Date? = nil
    @State private var layoutRefreshID = UUID()
    @State private var hoveredBubble: AppBubbleContext? = nil
    @State private var editingBubble: AppBubbleContext? = nil
    @State private var bubbleDraftNote = ""
    @FocusState private var bubbleNoteFocused: Bool
    // Configurable defaults
    @AppStorage("defaultGroupName") private var defaultGroupName = "Other"
@@ -255,6 +324,9 @@
    @State private var notchHeight: CGFloat = 0
    @AppStorage("displayMode") private var displayMode = "flat"
    @AppStorage("hideAppNames") private var hideAppNames = false
    private let editSidebarWidth: CGFloat = 188
    private let editSidebarHorizontalInset: CGFloat = 12
    private var isSideLayout: Bool {
        tagPosition == "left" || tagPosition == "right"
@@ -292,6 +364,9 @@
            case .editingApps:
                editAppsView
            }
            uncommonAppBubbleOverlay
            editActionFeedbackOverlay
            if let message = dropWarningToast {
                Text(message)
@@ -341,11 +416,17 @@
        }
        .onChange(of: editPhase) { _, newPhase in
            let active = newPhase != .none
            dismissAppBubble()
            NotificationCenter.default.post(
                name: .tagLauncherEditModeChanged,
                object: nil,
                userInfo: ["active": active]
            )
        }
        .onChange(of: bubbleNoteFocused) { _, focused in
            if editingBubble != nil && !focused {
                commitBubbleNote()
            }
        }
    }
@@ -502,6 +583,43 @@
        .id(layoutRefreshID)
    }
    private var uncommonAppBubbleOverlay: some View {
        GeometryReader { proxy in
            if let context = editingBubble ?? hoveredBubble {
                let rootFrame = proxy.frame(in: .global)
                let editing = editingBubble != nil
                let width = min(editing ? 440 : 520, max(260, proxy.size.width - 48))
                let placement = bubblePlacement(for: context.frame, rootFrame: rootFrame)
                let metrics = bubbleMetrics(
                    for: context,
                    rootFrame: rootFrame,
                    rootSize: proxy.size,
                    width: width,
                    placement: placement,
                    isEditing: editing
                )
                AppNameBubble(
                    appName: context.app.name,
                    note: currentNote(for: context.app),
                    isEditing: editing,
                    placement: placement,
                    draftNote: $bubbleDraftNote,
                    noteFocused: $bubbleNoteFocused,
                    onCommit: commitBubbleNote,
                    onCancel: dismissAppBubble
                )
                .frame(width: width)
                .fixedSize(horizontal: false, vertical: true)
                .position(x: metrics.centerX, y: metrics.centerY)
                .transition(.scale(scale: 0.94).combined(with: .opacity))
                .zIndex(500)
            }
        }
        .ignoresSafeArea()
        .allowsHitTesting(editingBubble != nil)
    }
    private var flatGrid: some View {
        ScrollViewReader { proxy in
            ScrollView {
@@ -515,6 +633,8 @@
                            showNames: !hideAppNames,
                            dragModeActive: appDragModeActive,
                            onDragModeChange: { setAppDragMode($0) },
                            onBubbleHover: handleBubbleHover,
                            onEditNote: beginEditingBubbleNote,
                            onDropApp: { path, source, copy in
                                dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
                            }
@@ -615,6 +735,8 @@
                        sourceTag: group.name,
                        dragModeActive: appDragModeActive,
                        onDragModeChange: { setAppDragMode($0) },
                        onBubbleHover: handleBubbleHover,
                        onEditNote: beginEditingBubbleNote,
                        onSelect: { openApp(app) }
                    )
                }
@@ -643,6 +765,7 @@
        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessActive ? 0.22 : 0),
                radius: (isColored && isHovered) || isColorlessActive ? 18 : 0,
                y: (isColored && isHovered) || isColorlessActive ? 10 : 0)
        .zIndex(isHovered ? 50 : 0)
        .scaleEffect(isColored && isHovered ? 1.015 : 1.0)
        .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isHovered)
        .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isColorlessFilled)
@@ -849,6 +972,8 @@
                                sourceTag: group.name,
                                dragModeActive: appDragModeActive,
                                onDragModeChange: { setAppDragMode($0) },
                                onBubbleHover: handleBubbleHover,
                                onEditNote: beginEditingBubbleNote,
                                onSelect: { openApp(app) }
                            )
                                .frame(width: cellWidth)
@@ -889,6 +1014,7 @@
        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessGridActive ? 0.22 : 0),
                radius: (isColored && isHovered) || isColorlessGridActive ? 18 : 0,
                y: (isColored && isHovered) || isColorlessGridActive ? 10 : 0)
        .zIndex(isHovered ? 50 : 0)
        .scaleEffect(isColored && isHovered ? 1.015 : 1.0)
        .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isHovered)
        .animation(.spring(response: 0.25, dampingFraction: 0.82), value: isColorlessFilled)
@@ -959,18 +1085,26 @@
    private var editAppsView: some View {
        VStack(spacing: 0) {
            HStack {
                Button { cancelEditApps(); setEditPhase(.none) } label: {
                    Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right")
                        .font(.system(size: 12))
            HStack(alignment: .center, spacing: 14) {
                HStack(alignment: .center, spacing: 12) {
                    Button { cancelEditApps(); setEditPhase(.none) } label: {
                        Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right")
                            .font(.system(size: 12))
                    }
                    .buttonStyle(.bordered)
                    operationHeaderPicker
                }
                .buttonStyle(.bordered)
                Spacer()
                Text(tr("edit.title")).font(.headline)
                Spacer()
                Button(tr("edit.confirm")) { confirmAssign() }
                Spacer(minLength: 16)
                headerModeHintView
                Button(editConfirmTitle) { confirmAssign() }
                    .buttonStyle(.borderedProminent)
                    .disabled(selectedAppPaths.isEmpty)
                    .disabled(selectedAppPaths.isEmpty || pendingOperationTagNames.isEmpty)
                    .fixedSize(horizontal: true, vertical: false)
                    .layoutPriority(2)
            }
            .padding(.horizontal, 24)
            .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20)
@@ -980,8 +1114,28 @@
            HStack(spacing: 0) {
                VStack(alignment: .leading, spacing: 4) {
                    Text(tr("edit.selectTags")).font(.caption).foregroundStyle(.secondary).padding(.bottom, 4)
                    Text(tr("edit.dragHint")).font(.caption2).foregroundStyle(.tertiary).padding(.bottom, 2)
                    VStack(alignment: .leading, spacing: 6) {
                        Text(tr("edit.selectTags"))
                            .font(.caption)
                            .foregroundStyle(.secondary)
                            .lineLimit(2)
                            .fixedSize(horizontal: false, vertical: true)
                            .multilineTextAlignment(.leading)
                            .frame(maxWidth: .infinity, alignment: .leading)
                        Text(tr("edit.dragHint"))
                            .font(.caption2)
                            .foregroundStyle(.tertiary)
                            .lineLimit(nil)
                            .fixedSize(horizontal: false, vertical: true)
                            .multilineTextAlignment(.leading)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    }
                    .padding(.horizontal, editSidebarHorizontalInset)
                    .padding(.top, 4)
                    .padding(.bottom, 4)
                    .frame(width: editSidebarWidth, alignment: .leading)
                    ScrollView(.vertical, showsIndicators: false) {
                        VStack(spacing: 4) {
                            ForEach(sortedTagNames, id: \.self) { tagName in
@@ -1006,8 +1160,9 @@
                            tagReorderFrames = frames
                        }
                    }
                    .frame(width: 155)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                }
                .frame(width: editSidebarWidth, alignment: .topLeading)
                Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1)
                if allApps.isEmpty {
@@ -1017,29 +1172,19 @@
                }
            }
            if let msg = successToast {
                Text(msg).font(.headline)
                    .padding(.horizontal, 20).padding(.vertical, 10)
                    .background(RoundedRectangle(cornerRadius: 10).fill(.ultraThickMaterial))
                    .transition(.move(edge: .bottom).combined(with: .opacity))
                    .onAppear {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                            withAnimation { successToast = nil }
                        }
                    }
            }
        }
    }
    private var editAppsGrid: some View {
        ScrollView {
            LazyVStack(alignment: .leading, spacing: 24) {
                ForEach(groups) { group in
                ForEach(editGroups) { group in
                    editFlatGroup(group)
                }
            }
            .padding(20)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
    }
    private func editFlatGroup(_ group: TagGroup) -> some View {
@@ -1068,31 +1213,71 @@
            ) {
                ForEach(group.apps) { app in editableAppItem(app) }
            }
            .frame(minHeight: group.apps.isEmpty ? max(52, iconSize + 12) : 0)
        }
    }
    private func selectableTagItem(_ tagName: String) -> some View {
        let isSelected = selectedTagNames.contains(tagName)
        let isRemovableCandidate = removableTagNames.contains(tagName)
        let isEnabled = editTagOperation == .add || isRemovableCandidate
        let showsPendingRemoval = editTagOperation == .remove && isRemovableCandidate && !isSelected
        let isUncommon = tagName == TagDatabase.uncommonTagKey
        let colorIndex = isUncommon ? 0 : (tagColors[tagName] ?? 0)
        return HStack(spacing: 6) {
            Circle()
                .fill(isSelected ? Color.accentColor : Color.secondary.opacity(0.3))
                .fill(isSelected ? Color.accentColor : Color.secondary.opacity(isEnabled ? 0.3 : 0.18))
                .frame(width: 16, height: 16)
                .overlay(isSelected ? Image(systemName: "checkmark").font(.system(size: 8, weight: .bold)).foregroundStyle(.white) : nil)
            Text(tagName).font(.system(size: 13, weight: .medium)).foregroundStyle(.primary)
            Text(displayTagName(tagName))
                .font(.system(size: 13, weight: isUncommon ? .semibold : .medium))
                .foregroundStyle(isEnabled ? Color.primary : Color.secondary.opacity(0.66))
                .lineLimit(1)
                .truncationMode(.tail)
                .frame(maxWidth: .infinity, alignment: .leading)
            HStack(spacing: 4) {
                if isUncommon {
                    Image(systemName: "questionmark.bubble.fill")
                        .font(.system(size: 11, weight: .semibold))
                        .foregroundStyle(isEnabled ? Color.secondary : Color.secondary.opacity(0.55))
                        .frame(width: 16, height: 16)
                }
                if showsPendingRemoval {
                    Image(systemName: "trash.fill")
                        .font(.system(size: 11, weight: .bold))
                        .foregroundStyle(.red)
                        .frame(width: 16, height: 16)
                        .transition(.scale(scale: 0.85).combined(with: .opacity))
                } else if editTagOperation == .remove {
                    Color.clear.frame(width: 16, height: 16)
                }
            }
            .frame(
                minWidth: editTagOperation == .remove
                    ? (isUncommon ? 36 : 16)
                    : (isUncommon ? 16 : 0),
                alignment: .trailing
            )
            .layoutPriority(1)
        }
        .padding(.horizontal, 10).padding(.vertical, 5)
        .padding(.horizontal, 10)
        .frame(height: 27)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(RoundedRectangle(cornerRadius: 6)
            .fill(Color(nsColor: TagColor.nsColor(for: tagColors[tagName] ?? 0).withAlphaComponent(0.3))))
            .fill(Color(nsColor: TagColor.nsColor(for: colorIndex).withAlphaComponent(isEnabled ? (isUncommon ? 0.22 : 0.3) : 0.12))))
        .contentShape(RoundedRectangle(cornerRadius: 6))
        .onTapGesture {
            guard isEnabled else { return }
            if isSelected { selectedTagNames.remove(tagName) } else { selectedTagNames.insert(tagName) }
        }
        .opacity(isEnabled ? 1.0 : 0.58)
        .animation(.easeInOut(duration: 0.15), value: showsPendingRemoval)
    }
    private func tagReorderGesture(for tagName: String) -> some Gesture {
        DragGesture(minimumDistance: 4, coordinateSpace: .named("tagReorderList"))
            .onChanged { value in
                guard canReorderTag(tagName) else { return }
                if dragItem == nil {
                    dragItem = tagName
                }
@@ -1100,6 +1285,7 @@
                reorderDraggedTag(at: value.location)
            }
            .onEnded { _ in
                guard canReorderTag(tagName) else { return }
                dragItem = nil
                TagEditor.reorderTags(draggedTagNames)
            }
@@ -1121,17 +1307,31 @@
    }
    private func confirmAssign() {
        guard !selectedAppPaths.isEmpty else { return }
        let paths = selectedAppPaths.map { $0.path }
        let tags = sortedTagNames.filter { selectedTagNames.contains($0) }
        TagEditor.setTags(tags, to: paths)
        selectedAppPaths = []; selectedTagNames = []
        let selectedApps = allApps.filter { selectedAppPaths.contains($0.path) }
        let tags = pendingOperationTagNames
        guard !selectedApps.isEmpty, !tags.isEmpty else { return }
        let paths = selectedApps.map { $0.path.path }
        let feedback = buildEditActionFeedback(for: selectedApps, tags: tags)
        switch editTagOperation {
        case .add:
            TagEditor.appendTags(tags, to: paths)
        case .remove:
            TagEditor.removeTags(tags, from: paths)
        }
        selectedAppPaths = []
        selectedTagNames = []
        removableTagNames = []
        refreshApps()
        withAnimation { successToast = tr("edit.success") }
        showEditActionFeedback(feedback)
    }
    private func cancelEditApps() {
        selectedAppPaths = []; selectedTagNames = []
        selectedAppPaths = []
        selectedTagNames = []
        removableTagNames = []
        editTagOperation = .add
    }
    private func editableAppItem(_ app: AppInfo) -> some View {
@@ -1169,20 +1369,219 @@
    }
    private func syncSelectedTagsFromSelectedApps() {
        let selectedApps = allApps.filter { selectedAppPaths.contains($0.path) }
        guard !selectedApps.isEmpty else {
        guard !selectedAppPaths.isEmpty else {
            selectedTagNames = []
            removableTagNames = []
            return
        }
        let editableTags = Set(sortedTagNames)
        if selectedApps.count == 1 {
            selectedTagNames = Set(selectedApps[0].tags.filter { editableTags.contains($0) })
        guard editTagOperation == .remove else {
            removableTagNames = []
            return
        }
        selectedTagNames = selectedApps.reduce(into: Set<String>()) { result, app in
            result.formUnion(app.tags.filter { editableTags.contains($0) })
        let selectedApps = allApps.filter { selectedAppPaths.contains($0.path) }
        let autoSelectedTags = selectedApps.reduce(into: Set<String>()) { result, app in
            result.formUnion(app.tags)
            if app.isUncommon {
                result.insert(TagDatabase.uncommonTagKey)
            }
        }
        removableTagNames = autoSelectedTags
        selectedTagNames = autoSelectedTags
    }
    private var operationHeaderPicker: some View {
        HStack(spacing: 8) {
            Text(tr("edit.operationLabel"))
                .font(.system(size: 12, weight: .medium))
                .foregroundStyle(.secondary)
            HStack(spacing: 6) {
                operationModeButton(.add, titleKey: "edit.modeAdd", systemImage: "plus.circle.fill")
                operationModeButton(.remove, titleKey: "edit.modeRemove", systemImage: "minus.circle.fill")
            }
        }
    }
    private func operationModeButton(_ mode: EditTagOperation, titleKey: String, systemImage: String) -> some View {
        let isActive = editTagOperation == mode
        return Button {
            setEditTagOperation(mode)
        } label: {
            Label(tr(titleKey), systemImage: systemImage)
                .font(.system(size: 12, weight: .semibold))
                .padding(.horizontal, 12)
                .padding(.vertical, 6)
                .foregroundStyle(isActive ? Color.white : Color.primary)
                .background(
                    Capsule(style: .continuous)
                        .fill(isActive ? Color.accentColor : Color.secondary.opacity(0.12))
                )
        }
        .buttonStyle(.plain)
    }
    private var editConfirmTitle: String {
        switch editTagOperation {
        case .add:
            return tr("edit.confirmAdd")
        case .remove:
            return tr("edit.confirmRemove")
        }
    }
    private func setEditTagOperation(_ mode: EditTagOperation) {
        guard editTagOperation != mode else { return }
        editTagOperation = mode
        if mode == .add {
            selectedTagNames = []
            removableTagNames = []
        } else {
            syncSelectedTagsFromSelectedApps()
        }
    }
    private var pendingOperationTagNames: [String] {
        switch editTagOperation {
        case .add:
            return sortedTagNames.filter { selectedTagNames.contains($0) }
        case .remove:
            return sortedTagNames.filter { removableTagNames.contains($0) && !selectedTagNames.contains($0) }
        }
    }
    private var editModeHintText: String {
        editTagOperation == .remove ? tr("edit.removeHint") : tr("edit.modeHint")
    }
    private var headerModeHintView: some View {
        Text(editModeHintText)
            .font(.system(size: 12, weight: .medium))
            .foregroundStyle(.secondary)
            .lineLimit(1)
            .truncationMode(.middle)
            .frame(minWidth: 0, maxWidth: 760, alignment: .trailing)
            .layoutPriority(1)
            .help(editModeHintText)
    }
    private var editActionFeedbackOverlay: some View {
        GeometryReader { proxy in
            if let feedback = editActionFeedback {
                ZStack {
                    Color.black.opacity(0.08)
                        .ignoresSafeArea()
                    EditActionFeedbackBubble(
                        title: feedback.title,
                        message: feedback.message,
                        onClose: dismissEditActionFeedback
                    )
                    .frame(width: min(760, max(360, proxy.size.width - 120)))
                    .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
                    .transition(.scale(scale: 0.94).combined(with: .opacity))
                }
                .zIndex(700)
            }
        }
        .ignoresSafeArea()
        .allowsHitTesting(editActionFeedback != nil)
    }
    private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback {
        let appNames = selectedApps.map(\.name)
            .joined(separator: localizedListSeparator)
        switch editTagOperation {
        case .add:
            let actualAddCount = selectedApps.reduce(into: 0) { count, app in
                count += tags.reduce(into: 0) { subtotal, tagName in
                    if !appHasTag(app, tagName: tagName) {
                        subtotal += 1
                    }
                }
            }
            return EditActionFeedback(
                title: tr("edit.confirmAdd"),
                message: formattedFeedbackMessage(
                    forKey: "edit.feedbackAddFormat",
                    replacements: [
                        "%appCount%": "\(selectedApps.count)",
                        "%appNames%": appNames,
                        "%tagCount%": "\(actualAddCount)"
                    ]
                )
            )
        case .remove:
            let actualRemovedTagNames = tags.filter { tagName in
                selectedApps.contains(where: { appHasTag($0, tagName: tagName) })
            }
            let actualRemoveCount = selectedApps.reduce(into: 0) { count, app in
                count += tags.reduce(into: 0) { subtotal, tagName in
                    if appHasTag(app, tagName: tagName) {
                        subtotal += 1
                    }
                }
            }
            return EditActionFeedback(
                title: tr("edit.confirmRemove"),
                message: formattedFeedbackMessage(
                    forKey: "edit.feedbackRemoveFormat",
                    replacements: [
                        "%appCount%": "\(selectedApps.count)",
                        "%appNames%": appNames,
                        "%tagCount%": "\(actualRemoveCount)",
                        "%tagNames%": actualRemovedTagNames
                            .map(displayTagName)
                            .joined(separator: localizedListSeparator)
                    ]
                )
            )
        }
    }
    private func appHasTag(_ app: AppInfo, tagName: String) -> Bool {
        if tagName == TagDatabase.uncommonTagKey {
            return app.isUncommon
        }
        return app.tags.contains(tagName)
    }
    private func formattedFeedbackMessage(forKey key: String, replacements: [String: String]) -> String {
        replacements.reduce(tr(key)) { partial, replacement in
            partial.replacingOccurrences(of: replacement.key, with: replacement.value)
        }
    }
    private func showEditActionFeedback(_ feedback: EditActionFeedback) {
        let feedbackID = feedback.id
        withAnimation(.spring(response: 0.24, dampingFraction: 0.82)) {
            editActionFeedback = feedback
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.6) {
            guard editActionFeedback?.id == feedbackID else { return }
            dismissEditActionFeedback()
        }
    }
    private func dismissEditActionFeedback() {
        withAnimation(.easeOut(duration: 0.18)) {
            editActionFeedback = nil
        }
    }
    private var localizedListSeparator: String {
        switch L10n.currentCode {
        case "zh-Hans", "zh-Hant", "ja":
            return "、"
        case "ar", "ar-Najdi":
            return "، "
        default:
            return ", "
        }
    }
@@ -1193,7 +1592,11 @@
        // Use user-defined order, fall back to alpha
        let ordered = draggedTagNames.filter { filtered.contains($0) }
        let remaining = filtered.filter { !ordered.contains($0) }.sorted()
        return ordered + remaining
        return ordered + remaining + [TagDatabase.uncommonTagKey]
    }
    private func displayTagName(_ tagName: String) -> String {
        tagName == TagDatabase.uncommonTagKey ? tr("group.uncommon") : tagName
    }
    private var tagLabels: [TagLabel] {
@@ -1217,7 +1620,98 @@
        }
    }
    private var editGroups: [TagGroup] {
        groups + [
            TagGroup(
                name: tr("group.uncommon"),
                apps: allApps.filter(\.isUncommon).sorted {
                    $0.name.localizedStandardCompare($1.name) == .orderedAscending
                }
            )
        ]
    }
    // MARK: - Actions
    private func handleBubbleHover(app: AppInfo, frame: CGRect, hovering: Bool) {
        guard editingBubble == nil else { return }
        if hovering {
            hoveredBubble = AppBubbleContext(app: app, frame: frame)
        } else if hoveredBubble?.app.path == app.path {
            hoveredBubble = nil
        }
    }
    private func beginEditingBubbleNote(app: AppInfo, frame: CGRect) {
        bubbleDraftNote = currentNote(for: app)
        hoveredBubble = nil
        editingBubble = AppBubbleContext(app: app, frame: frame)
        DispatchQueue.main.async {
            bubbleNoteFocused = true
        }
    }
    private func commitBubbleNote() {
        guard let context = editingBubble else { return }
        let limited = String(bubbleDraftNote.prefix(TagDatabase.maxAppNoteLength))
            .trimmingCharacters(in: .whitespacesAndNewlines)
        TagEditor.setAppNote(limited, for: context.app.path.path)
        bubbleDraftNote = limited
        editingBubble = nil
        bubbleNoteFocused = false
        refreshApps()
    }
    private func dismissAppBubble() {
        hoveredBubble = nil
        editingBubble = nil
        bubbleNoteFocused = false
    }
    private func currentNote(for app: AppInfo) -> String {
        if let latest = allApps.first(where: { $0.path == app.path })?.note {
            return latest
        }
        return app.note ?? ""
    }
    private func bubblePlacement(for frame: CGRect, rootFrame: CGRect) -> BubblePlacement {
        frame.minY - rootFrame.minY < 170 ? .below : .above
    }
    private func bubbleMetrics(
        for context: AppBubbleContext,
        rootFrame: CGRect,
        rootSize: CGSize,
        width: CGFloat,
        placement: BubblePlacement,
        isEditing: Bool
    ) -> (centerX: CGFloat, centerY: CGFloat) {
        let appCenterX = context.frame.midX - rootFrame.minX
        let halfWidth = width / 2
        let centerX = min(max(appCenterX, halfWidth + 24), max(halfWidth + 24, rootSize.width - halfWidth - 24))
        let estimatedHeight = estimatedBubbleHeight(for: context.app, width: width, isEditing: isEditing)
        let topY: CGFloat
        if placement == .below {
            topY = context.frame.maxY - rootFrame.minY + 10
        } else {
            topY = context.frame.minY - rootFrame.minY - estimatedHeight - 10
        }
        let clampedTopY = min(max(topY, 14), max(14, rootSize.height - estimatedHeight - 14))
        return (centerX, clampedTopY + estimatedHeight / 2)
    }
    private func estimatedBubbleHeight(for app: AppInfo, width: CGFloat, isEditing: Bool) -> CGFloat {
        if isEditing {
            return 120
        }
        let note = currentNote(for: app).trimmingCharacters(in: .whitespacesAndNewlines)
        guard !note.isEmpty else { return 68 }
        let contentWidth = max(160, width - 48)
        let approxCharsPerLine = max(16, Int(contentWidth / 14))
        let lines = min(4, max(1, Int(ceil(Double(note.count) / Double(approxCharsPerLine)))))
        return 70 + CGFloat(lines) * 20
    }
    func scrollTo(_ id: String) {
        withAnimation(.easeInOut(duration: 0.25)) { scrollProxy?.scrollTo(id, anchor: .top) }
@@ -1405,9 +1899,9 @@
    func refreshApps(forceLayoutRefresh: Bool = false) {
        DispatchQueue.global(qos: .userInitiated).async {
            var apps = AppIndexer.scan()
            let store = TagDatabase.load()
            apps = TagEditor.annotate(apps: apps)
            let scannedApps = AppIndexer.scan()
            let store = TagEditor.reconcileScannedApps(scannedApps)
            let apps = TagEditor.annotate(apps: scannedApps, store: store)
            let colors = store.tags.mapValues { $0.color }
            let order = TagEditor.orderedTagNames()
            DispatchQueue.main.async {
@@ -1428,6 +1922,9 @@
        appDragModeActive = false
        endTagNavReorder()
        hideOverlay()
        DispatchQueue.global(qos: .utility).async {
            TagEditor.recordLauncherOpen(for: app.path.path)
        }
        let configuration = NSWorkspace.OpenConfiguration()
        NSWorkspace.shared.openApplication(at: app.path, configuration: configuration)
    }
Apptag/DataLayer.swift
@@ -10,11 +10,13 @@
    let tags: [String]
    let bundleIdentifier: String?
    let icon: NSImage  // Pre-loaded during background scan
    var isUncommon: Bool = false
    var note: String? = nil
    /// True if this is an Apple pre-installed app.
    var isAppleApp: Bool {
        if let bid = bundleIdentifier, bid.hasPrefix("com.apple.") { return true }
        return path.path.hasPrefix("/System/Applications/")
        return AppIndexer.isSystemAppPath(path.path)
    }
    func hash(into hasher: inout Hasher) { hasher.combine(path) }
@@ -53,45 +55,95 @@
    static let searchPaths: [URL] = [
        URL(fileURLWithPath: "/Applications"),
        URL(fileURLWithPath: "/System/Applications"),
        URL(fileURLWithPath: "/System/Cryptexes/App/System/Applications"),
        URL(fileURLWithPath: "/System/Volumes/Preboot/Cryptexes/App/System/Applications"),
        FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent("Applications")
    ]
    private static let systemAppPathPrefixes = [
        "/System/Applications/",
        "/System/Cryptexes/App/System/Applications/",
        "/System/Volumes/Preboot/Cryptexes/App/System/Applications/"
    ]
    static func isSystemAppPath(_ path: String) -> Bool {
        systemAppPathPrefixes.contains { path.hasPrefix($0) }
    }
    /// Scan all standard locations. Tags are annotated from TagDatabase by the caller.
    static func scan() -> [AppInfo] {
        var seen = Set<URL>()
        var seenResolvedPaths = Set<String>()
        var apps: [AppInfo] = []
        for baseURL in searchPaths {
            scanDirectChildren(
                in: baseURL,
                apps: &apps,
                seenResolvedPaths: &seenResolvedPaths
            )
            guard let enumerator = FileManager.default.enumerator(
                at: baseURL,
                includingPropertiesForKeys: [.isDirectoryKey],
                includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey],
                options: [.skipsHiddenFiles, .skipsPackageDescendants]
            ) else { continue }
            for case let url as URL in enumerator {
                guard url.pathExtension == "app" else { continue }
                guard !seen.contains(url) else { continue }
                seen.insert(url)
                let name = url.deletingPathExtension().lastPathComponent
                let icon = NSWorkspace.shared.icon(forFile: url.path)
                icon.size = NSSize(width: 96, height: 96)
                var bundleId: String? = nil
                if let bundle = Bundle(url: url) {
                    bundleId = bundle.bundleIdentifier
                }
                apps.append(AppInfo(
                    name: name, path: url, tags: [],
                    bundleIdentifier: bundleId, icon: icon
                ))
                appendAppIfNeeded(
                    at: url,
                    apps: &apps,
                    seenResolvedPaths: &seenResolvedPaths
                )
            }
        }
        return apps.sorted {
            $0.name.localizedStandardCompare($1.name) == .orderedAscending
        }
    }
    private static func scanDirectChildren(
        in baseURL: URL,
        apps: inout [AppInfo],
        seenResolvedPaths: inout Set<String>
    ) {
        guard let names = try? FileManager.default.contentsOfDirectory(atPath: baseURL.path) else { return }
        for name in names where !name.hasPrefix(".") {
            appendAppIfNeeded(
                at: baseURL.appendingPathComponent(name),
                apps: &apps,
                seenResolvedPaths: &seenResolvedPaths
            )
        }
    }
    private static func appendAppIfNeeded(
        at url: URL,
        apps: inout [AppInfo],
        seenResolvedPaths: inout Set<String>
    ) {
        guard url.pathExtension.lowercased() == "app" else { return }
        let displayURL = url.standardizedFileURL
        let resolvedURL = displayURL.resolvingSymlinksInPath().standardizedFileURL
        guard seenResolvedPaths.insert(resolvedURL.path).inserted else { return }
        let name = displayURL.deletingPathExtension().lastPathComponent
        let icon = NSWorkspace.shared.icon(forFile: displayURL.path)
        icon.size = NSSize(width: 96, height: 96)
        let bundleId = Bundle(url: displayURL)?.bundleIdentifier
            ?? Bundle(url: resolvedURL)?.bundleIdentifier
        apps.append(AppInfo(
            name: name,
            path: displayURL,
            tags: [],
            bundleIdentifier: bundleId,
            icon: icon
        ))
    }
    /// Group apps by their tags (from TagDatabase).
@@ -140,6 +192,14 @@
}
enum TagDatabase {
    static let uncommonTagKey = "__system.uncommon"
    static let maxAppNoteLength = 80
    static let autoUncommonOpenThreshold = 100
    enum UncommonSource: String, Codable {
        case auto
        case manual
    }
    // MARK: Storage types
@@ -152,6 +212,42 @@
        var tags: [String: TagDef] = [:]
        var appTags: [String: [String]] = [:]  // path → tag names
        var tagOrder: [String] = []  // display order; empty → alpha sort
        var uncommonAppPaths: [String] = []  // special marker; does not affect normal groups
        var uncommonSources: [String: UncommonSource] = [:]  // current uncommon source: auto/manual
        var appOpenCounts: [String: Int] = [:]  // launches opened from TagLauncher
        var knownAppPaths: [String] = []  // baseline set to detect newly installed apps
        var appNotes: [String: String] = [:]  // path → user note; retained even if marker is removed
        enum CodingKeys: String, CodingKey {
            case version
            case tags
            case appTags
            case tagOrder
            case uncommonAppPaths
            case uncommonSources
            case appOpenCounts
            case knownAppPaths
            case appNotes
        }
        init() {}
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1
            tags = try container.decodeIfPresent([String: TagDef].self, forKey: .tags) ?? [:]
            appTags = try container.decodeIfPresent([String: [String]].self, forKey: .appTags) ?? [:]
            tagOrder = try container.decodeIfPresent([String].self, forKey: .tagOrder) ?? []
            uncommonAppPaths = try container.decodeIfPresent([String].self, forKey: .uncommonAppPaths) ?? []
            uncommonSources = try container.decodeIfPresent([String: UncommonSource].self, forKey: .uncommonSources) ?? [:]
            appOpenCounts = try container.decodeIfPresent([String: Int].self, forKey: .appOpenCounts) ?? [:]
            knownAppPaths = try container.decodeIfPresent([String].self, forKey: .knownAppPaths) ?? []
            appNotes = try container.decodeIfPresent([String: String].self, forKey: .appNotes) ?? [:]
            for path in uncommonAppPaths where uncommonSources[path] == nil {
                uncommonSources[path] = .manual
            }
        }
    }
    // MARK: Paths
@@ -191,7 +287,9 @@
    }
    static func save(_ store: Store) {
        guard let data = try? JSONEncoder().encode(store) else { return }
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        guard let data = try? encoder.encode(store) else { return }
        try? data.write(to: storeURL, options: .atomic)
    }
@@ -299,12 +397,18 @@
    /// Annotate scanned apps with tags from the database.
    static func annotate(apps: [AppInfo]) -> [AppInfo] {
        let store = TagDatabase.load()
        annotate(apps: apps, store: TagDatabase.load())
    }
    static func annotate(apps: [AppInfo], store: TagDatabase.Store) -> [AppInfo] {
        let uncommonPaths = Set(store.uncommonAppPaths)
        return apps.map { app in
            let appTags = store.appTags[app.path.path] ?? []
            return AppInfo(
                name: app.name, path: app.path, tags: appTags,
                bundleIdentifier: app.bundleIdentifier, icon: app.icon
                bundleIdentifier: app.bundleIdentifier, icon: app.icon,
                isUncommon: uncommonPaths.contains(app.path.path),
                note: store.appNotes[app.path.path]
            )
        }
    }
@@ -333,13 +437,138 @@
    /// Replace the full editable tag set for multiple apps.
    static func setTags(_ tags: [String], to paths: [String]) {
        var store = TagDatabase.load()
        let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
        let validTags = tags.filter { store.tags[$0] != nil }
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in paths {
            if validTags.isEmpty {
                store.appTags.removeValue(forKey: path)
            } else {
                store.appTags[path] = validTags
            }
            if selectedUncommon {
                uncommonPaths.insert(path)
                store.uncommonSources[path] = .manual
            } else {
                uncommonPaths.remove(path)
                store.uncommonSources.removeValue(forKey: path)
            }
        }
        store.uncommonAppPaths = uncommonPaths.sorted()
        TagDatabase.save(store)
    }
    /// Append tags to multiple apps without disturbing their existing tag sets.
    static func appendTags(_ tags: [String], to paths: [String]) {
        guard !tags.isEmpty, !paths.isEmpty else { return }
        var store = TagDatabase.load()
        let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
        let validTags = tags.filter { store.tags[$0] != nil }
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in paths {
            var current = store.appTags[path] ?? []
            for tag in validTags where !current.contains(tag) {
                current.append(tag)
            }
            if !current.isEmpty {
                store.appTags[path] = current
            }
            if selectedUncommon {
                uncommonPaths.insert(path)
                store.uncommonSources[path] = .manual
            }
        }
        store.uncommonAppPaths = uncommonPaths.sorted()
        TagDatabase.save(store)
    }
    /// Remove tags from multiple apps while preserving every unrelated tag.
    static func removeTags(_ tags: [String], from paths: [String]) {
        guard !tags.isEmpty, !paths.isEmpty else { return }
        var store = TagDatabase.load()
        let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
        let validTags = Set(tags.filter { store.tags[$0] != nil })
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in paths {
            var current = store.appTags[path] ?? []
            current.removeAll { validTags.contains($0) }
            if current.isEmpty {
                store.appTags.removeValue(forKey: path)
            } else {
                store.appTags[path] = current
            }
            if selectedUncommon {
                uncommonPaths.remove(path)
                store.uncommonSources.removeValue(forKey: path)
            }
        }
        store.uncommonAppPaths = uncommonPaths.sorted()
        TagDatabase.save(store)
    }
    static func reconcileScannedApps(_ apps: [AppInfo]) -> TagDatabase.Store {
        var store = TagDatabase.load()
        let scannedPaths = Set(apps.map { $0.path.path })
        let knownPaths = Set(store.knownAppPaths)
        if store.knownAppPaths.isEmpty {
            store.knownAppPaths = scannedPaths.sorted()
            if apps.isEmpty == false {
                TagDatabase.save(store)
            }
            return store
        }
        let newPaths = scannedPaths.subtracting(knownPaths)
        guard !newPaths.isEmpty else { return store }
        var uncommonPaths = Set(store.uncommonAppPaths)
        for path in newPaths {
            uncommonPaths.insert(path)
            store.uncommonSources[path] = .auto
            if store.appOpenCounts[path] == nil {
                store.appOpenCounts[path] = 0
            }
        }
        store.uncommonAppPaths = uncommonPaths.sorted()
        store.knownAppPaths = knownPaths.union(newPaths).sorted()
        TagDatabase.save(store)
        return store
    }
    static func recordLauncherOpen(for path: String) {
        var store = TagDatabase.load()
        store.appOpenCounts[path] = (store.appOpenCounts[path] ?? 0) + 1
        if store.uncommonSources[path] == .auto,
           store.appOpenCounts[path, default: 0] >= TagDatabase.autoUncommonOpenThreshold {
            var uncommonPaths = Set(store.uncommonAppPaths)
            uncommonPaths.remove(path)
            store.uncommonAppPaths = uncommonPaths.sorted()
            store.uncommonSources.removeValue(forKey: path)
        }
        TagDatabase.save(store)
    }
    static func setAppNote(_ note: String, for path: String) {
        var store = TagDatabase.load()
        let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
        let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength))
        if limited.isEmpty {
            store.appNotes.removeValue(forKey: path)
        } else {
            store.appNotes[path] = limited
        }
        TagDatabase.save(store)
    }
Apptag/Info.plist
@@ -19,9 +19,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>5.7.8</string>
    <string>6.0.5</string>
    <key>CFBundleVersion</key>
    <string>583</string>
    <string>610</string>
    <key>LSMinimumSystemVersion</key>
    <string>15.0</string>
    <key>NSHighResolutionCapable</key>
Apptag/Localization/ar-Najdi.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "فشل التصدير",
  "settings.import": "استيراد التصنيفات والتخطيط…",
  "settings.backupDesc": "التصدير يحفظ التصنيفات والتخطيط بملف JSON. الاستيراد يستبدل الإعداد الحالي بملف.",
  "settings.uncommonBubble": "فقاعات للتطبيقات القليلة الاستخدام",
  "settings.uncommonBubbleDesc": "ورّ فقاعة كبيرة للاسم والملاحظة بس للتطبيقات المحددة كقليلة الاستخدام.",
  "settings.importFailed": "فشل الاستيراد",
  "edit.exit": "إنهاء التحرير",
  "edit.tags": "تحرير الوسوم",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "غير مصنف",
  "group.appleBuiltIn": "مدمج",
  "group.uncommon": "ما ينستخدم واجد",
  "drop.systemDefaultWarning": "لا تسحب التطبيقات لتصنيفات النظام الافتراضية",
  "tag.design": "تصميم",
  "tag.development": "تطوير",
@@ -64,5 +67,22 @@
  "tag.productivity": "إنتاجية",
  "settings.coloredContainer": "حاوية ملونة",
  "settings.gridContainer": "شبكة بدون لون",
  "settings.coloredGridContainer": "شبكة ملونة"
  "settings.coloredGridContainer": "شبكة ملونة",
  "appNote.edit": "عدّل ملاحظة الغرض…",
  "appNote.placeholder": "ملاحظة غرض التطبيق",
  "edit.modeAdd": "إضافة وسوم",
  "edit.modeRemove": "إزالة وسوم",
  "edit.modeHint": "علامات التحديد في اليسار تنطبق على هالعملية فقط.",
  "edit.currentTags": "الوسوم الحالية",
  "edit.commonTags": "الوسوم المشتركة",
  "edit.selectionHint": "اختر التطبيقات من اليمين، ثم اختر من اليسار الوسوم اللي تبي تضيفها أو تزيلها.",
  "edit.noAssignedTags": "التطبيق المحدد ما عليه وسوم للحين.",
  "edit.noCommonTags": "ما فيه وسوم مشتركة بين التطبيقات المحددة.",
  "edit.mixedTagsHint": "باقي الوسوم تختلف بين التطبيقات المحددة.",
  "edit.operationLabel": "هالاجراء:",
  "edit.confirmAdd": "تأكيد الإضافة",
  "edit.confirmRemove": "تأكيد الإزالة",
  "edit.feedbackAddFormat": "هالعملية اضافت %tagCount% من الوسوم على %appCount% تطبيقات (%appNames%).",
  "edit.feedbackRemoveFormat": "هالعملية شالت %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم اللي انشالت: %tagNames%.",
  "edit.removeHint": "هنا ما تقدر تشيل الصح الا من الوسوم الموجودة من قبل. اي وسم يطلع معه رمز حذف احمر بينشال بعد التاكيد."
}
Apptag/Localization/ar.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "فشل التصدير",
  "settings.import": "استيراد التصنيفات والتخطيط…",
  "settings.backupDesc": "يحفظ التصدير التصنيفات والتخطيط في ملف JSON. يستبدل الاستيراد الإعداد الحالي بملف.",
  "settings.uncommonBubble": "فقاعات التطبيقات قليلة الاستخدام",
  "settings.uncommonBubbleDesc": "اعرض فقاعة كبيرة للاسم والملاحظة فقط للتطبيقات المحددة كقليلة الاستخدام.",
  "settings.importFailed": "فشل الاستيراد",
  "edit.exit": "إنهاء التحرير",
  "edit.tags": "تحرير الوسوم",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "غير مصنف",
  "group.appleBuiltIn": "مدمج",
  "group.uncommon": "غير مستخدم غالبًا",
  "drop.systemDefaultWarning": "لا تسحب التطبيقات إلى التصنيفات الافتراضية للنظام",
  "tag.design": "تصميم",
  "tag.development": "تطوير",
@@ -64,5 +67,22 @@
  "tag.productivity": "إنتاجية",
  "settings.coloredContainer": "حاوية ملونة",
  "settings.gridContainer": "شبكة بلا لون",
  "settings.coloredGridContainer": "شبكة ملونة"
  "settings.coloredGridContainer": "شبكة ملونة",
  "appNote.edit": "تحرير ملاحظة الغرض…",
  "appNote.placeholder": "ملاحظة غرض التطبيق",
  "edit.modeAdd": "إضافة وسوم",
  "edit.modeRemove": "إزالة وسوم",
  "edit.modeHint": "علامات التحديد على اليسار تنطبق فقط على هذه العملية.",
  "edit.currentTags": "الوسوم الحالية",
  "edit.commonTags": "الوسوم المشتركة",
  "edit.selectionHint": "اختر التطبيقات من اليمين، ثم اختر من اليسار الوسوم التي تريد إضافتها أو إزالتها.",
  "edit.noAssignedTags": "التطبيق المحدد لا يملك وسوما بعد.",
  "edit.noCommonTags": "لا توجد وسوم مشتركة بين التطبيقات المحددة.",
  "edit.mixedTagsHint": "الوسوم الأخرى تختلف بين التطبيقات المحددة.",
  "edit.operationLabel": "هذا الاجراء:",
  "edit.confirmAdd": "تأكيد الإضافة",
  "edit.confirmRemove": "تأكيد الإزالة",
  "edit.feedbackAddFormat": "اضافت هذه العملية %tagCount% من الوسوم عبر %appCount% تطبيقات (%appNames%).",
  "edit.feedbackRemoveFormat": "ازالت هذه العملية %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم التي تمت ازالتها: %tagNames%.",
  "edit.removeHint": "هنا يمكنك الغاء تحديد الوسوم الموجودة بالفعل فقط. الوسوم التي يظهر بجانبها رمز حذف احمر ستزال بعد التاكيد."
}
Apptag/Localization/cs.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Export se nezdařil",
  "settings.import": "Importovat kategorie a rozvržení…",
  "settings.backupDesc": "Export uloží kategorie a rozvržení do souboru JSON. Import nahradí aktuální nastavení souborem.",
  "settings.uncommonBubble": "Bubliny pro málo používané aplikace",
  "settings.uncommonBubbleDesc": "Zobrazí velké jméno a poznámku jen u aplikací označených jako málo používané.",
  "settings.importFailed": "Import se nezdařil",
  "edit.exit": "Ukončit úpravy",
  "edit.tags": "Upravit štítky",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Nezařazeno",
  "group.appleBuiltIn": "Vestavěné",
  "group.uncommon": "Málo používané",
  "drop.systemDefaultWarning": "Nepřetahujte aplikace do výchozích systémových kategorií",
  "tag.design": "Design",
  "tag.development": "Vývoj",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivita",
  "settings.coloredContainer": "Barevný kontejner",
  "settings.gridContainer": "Bezbarvá mřížka",
  "settings.coloredGridContainer": "Barevná mřížka"
  "settings.coloredGridContainer": "Barevná mřížka",
  "appNote.edit": "Upravit poznámku k účelu…",
  "appNote.placeholder": "Poznámka k účelu aplikace",
  "edit.modeAdd": "Pridat stitky",
  "edit.modeRemove": "Odebrat stitky",
  "edit.modeHint": "Zatrzeni vlevo plati jen pro tuto operaci.",
  "edit.currentTags": "Aktualni stitky",
  "edit.commonTags": "Spolecne stitky",
  "edit.selectionHint": "Vyberte aplikace vpravo a pak vlevo zvolte stitky, ktere chcete pridat nebo odebrat.",
  "edit.noAssignedTags": "Vybrana aplikace zatim nema zadne stitky.",
  "edit.noCommonTags": "Vybrane aplikace nemaji zadne spolecne stitky.",
  "edit.mixedTagsHint": "Ostatni stitky se mezi vybranymi aplikacemi lisi.",
  "edit.operationLabel": "Akce:",
  "edit.confirmAdd": "Potvrdit pridani",
  "edit.confirmRemove": "Potvrdit odebrani",
  "edit.feedbackAddFormat": "Tato akce pridala %tagCount% stitku do %appCount% aplikaci (%appNames%).",
  "edit.feedbackRemoveFormat": "Tato akce odebrala %tagCount% stitku z %appCount% aplikaci (%appNames%). Odebrane stitky: %tagNames%.",
  "edit.removeHint": "Zde lze odskrtnout jen jiz existujici stitky. Stitky s cervenou ikonou odstraneni budou po potvrzeni odebrany."
}
Apptag/Localization/da.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Eksport mislykkedes",
  "settings.import": "Importer kategorier og layout…",
  "settings.backupDesc": "Eksport gemmer kategorier og layout i en JSON-fil. Import erstatter den aktuelle opsætning med en fil.",
  "settings.uncommonBubble": "Bobler for sjældent brugte apps",
  "settings.uncommonBubbleDesc": "Viser stor navne- og noteboble kun for apps markeret som sjældent brugt.",
  "settings.importFailed": "Import mislykkedes",
  "edit.exit": "Afslut redigering",
  "edit.tags": "Rediger tags",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Ukategoriseret",
  "group.appleBuiltIn": "Indbygget",
  "group.uncommon": "Sjældent brugt",
  "drop.systemDefaultWarning": "Træk ikke apps til systemets standardkategorier",
  "tag.design": "Design",
  "tag.development": "Udvikling",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitet",
  "settings.coloredContainer": "Farvet beholder",
  "settings.gridContainer": "Farveløst gitter",
  "settings.coloredGridContainer": "Farvet gitter"
  "settings.coloredGridContainer": "Farvet gitter",
  "appNote.edit": "Rediger formålsnote…",
  "appNote.placeholder": "Formålsnote for app",
  "edit.modeAdd": "Tilfoj tags",
  "edit.modeRemove": "Fjern tags",
  "edit.modeHint": "Afkrydsningerne til venstre gaelder kun for denne handling.",
  "edit.currentTags": "Nuvaerende tags",
  "edit.commonTags": "Faelles tags",
  "edit.selectionHint": "Vaelg apps til hojre, og vaelg derefter til venstre de tags, du vil tilfoje eller fjerne.",
  "edit.noAssignedTags": "Den valgte app har ingen tags endnu.",
  "edit.noCommonTags": "De valgte apps har ingen faelles tags.",
  "edit.mixedTagsHint": "Andre tags varierer mellem de valgte apps.",
  "edit.operationLabel": "Handling:",
  "edit.confirmAdd": "Bekraeft tilfojelse",
  "edit.confirmRemove": "Bekraeft fjernelse",
  "edit.feedbackAddFormat": "Denne handling tilfojede %tagCount% tags til %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handling fjernede %tagCount% tags fra %appCount% apps (%appNames%). Fjernede tags: %tagNames%.",
  "edit.removeHint": "Her kan kun eksisterende tags fjernes fra markeringen. Tags med et rodt sletteikon fjernes efter bekraeftelse."
}
Apptag/Localization/de.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Export fehlgeschlagen",
  "settings.import": "Kategorien & Layout importieren…",
  "settings.backupDesc": "Der Export speichert Kategorien und Layout als JSON-Datei. Der Import ersetzt die aktuelle Konfiguration durch eine Datei.",
  "settings.uncommonBubble": "Hinweisblasen fuer seltene Apps",
  "settings.uncommonBubbleDesc": "Zeigt Name und Notiz nur fuer Apps, die als selten genutzt markiert sind.",
  "settings.importFailed": "Import fehlgeschlagen",
  "edit.exit": "Bearbeitung beenden",
  "edit.tags": "Tags bearbeiten",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Nicht kategorisiert",
  "group.appleBuiltIn": "Integriert",
  "group.uncommon": "Selten genutzt",
  "drop.systemDefaultWarning": "Ziehe keine Apps in die Standard-Systemkategorien",
  "tag.design": "Design",
  "tag.development": "Entwicklung",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivität",
  "settings.coloredContainer": "Farbiger Container",
  "settings.gridContainer": "Farbloses Raster",
  "settings.coloredGridContainer": "Farbiges Raster"
  "settings.coloredGridContainer": "Farbiges Raster",
  "appNote.edit": "Verwendungsnotiz bearbeiten…",
  "appNote.placeholder": "Verwendungsnotiz zur App",
  "edit.modeAdd": "Tags hinzufuegen",
  "edit.modeRemove": "Tags entfernen",
  "edit.modeHint": "Die Haken links gelten nur fuer diesen Vorgang.",
  "edit.currentTags": "Aktuelle Tags",
  "edit.commonTags": "Gemeinsame Tags",
  "edit.selectionHint": "Waehlen Sie rechts Apps aus und links die Tags, die in diesem Schritt hinzugefuegt oder entfernt werden sollen.",
  "edit.noAssignedTags": "Die ausgewaehlte App hat noch keine Tags.",
  "edit.noCommonTags": "Die ausgewaehlten Apps haben keine gemeinsamen Tags.",
  "edit.mixedTagsHint": "Weitere Tags unterscheiden sich zwischen den ausgewaehlten Apps.",
  "edit.operationLabel": "Aktion:",
  "edit.confirmAdd": "Hinzufuegen bestaetigen",
  "edit.confirmRemove": "Entfernen bestaetigen",
  "edit.feedbackAddFormat": "Dieser Vorgang hat %tagCount% Tags zu %appCount% Apps hinzugefugt (%appNames%).",
  "edit.feedbackRemoveFormat": "Dieser Vorgang hat %tagCount% Tags von %appCount% Apps entfernt (%appNames%). Entfernte Tags: %tagNames%.",
  "edit.removeHint": "Hier konnen nur bereits vorhandene Tags abgewahlt werden. Tags mit rotem Loschsymbol werden nach der Bestatigung entfernt."
}
Apptag/Localization/en.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Export Failed",
  "settings.import": "Import Categories & Layout…",
  "settings.backupDesc": "Export saves your categories and layout to a JSON file. Import replaces the current setup with one from a file.",
  "settings.uncommonBubble": "Uncommon app bubble tips",
  "settings.uncommonBubbleDesc": "Show a large name and note bubble only for apps marked as Uncommon.",
  "settings.importFailed": "Import Failed",
  "edit.exit": "Exit editing",
  "edit.tags": "Edit Tags",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Uncategorized",
  "group.appleBuiltIn": "Built-in",
  "group.uncommon": "Uncommon",
  "drop.systemDefaultWarning": "Do not drag apps into system default categories",
  "tag.design": "Design",
  "tag.development": "Development",
@@ -64,5 +67,22 @@
  "tag.productivity": "Productivity",
  "settings.coloredContainer": "Colored Container",
  "settings.gridContainer": "Colorless Grid",
  "settings.coloredGridContainer": "Colored Grid"
  "settings.coloredGridContainer": "Colored Grid",
  "appNote.edit": "Edit purpose note…",
  "appNote.placeholder": "App purpose note",
  "edit.modeAdd": "Add tags",
  "edit.modeRemove": "Remove tags",
  "edit.modeHint": "Left-side checkmarks apply only to this operation.",
  "edit.currentTags": "Current tags",
  "edit.commonTags": "Shared tags",
  "edit.selectionHint": "Select apps on the right, then choose the tags to add or remove on the left.",
  "edit.noAssignedTags": "The selected app has no tags yet.",
  "edit.noCommonTags": "These selected apps have no shared tags.",
  "edit.mixedTagsHint": "Other tags differ between the selected apps.",
  "edit.operationLabel": "Action:",
  "edit.confirmAdd": "Confirm Add",
  "edit.confirmRemove": "Confirm Remove",
  "edit.feedbackAddFormat": "This operation added %tagCount% tags across %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "This operation removed %tagCount% tags across %appCount% apps (%appNames%). Removed tags: %tagNames%.",
  "edit.removeHint": "Only existing tags can be unchecked here. Tags showing a red delete icon will be removed after confirmation."
}
Apptag/Localization/es.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Error de exportación",
  "settings.import": "Importar categorías y diseño…",
  "settings.backupDesc": "La exportación guarda las categorías y el diseño en un archivo JSON. La importación reemplaza la configuración actual.",
  "settings.uncommonBubble": "Burbujas para apps poco usadas",
  "settings.uncommonBubbleDesc": "Muestra una burbuja grande con nombre y nota solo para apps marcadas como poco usadas.",
  "settings.importFailed": "Error de importación",
  "edit.exit": "Salir de edición",
  "edit.tags": "Editar etiquetas",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Sin categorizar",
  "group.appleBuiltIn": "Integradas",
  "group.uncommon": "Poco usadas",
  "drop.systemDefaultWarning": "No arrastres a las categorías predeterminadas del sistema",
  "tag.design": "Diseño",
  "tag.development": "Desarrollo",
@@ -64,5 +67,22 @@
  "tag.productivity": "Oficina",
  "settings.coloredContainer": "Contenedor coloreado",
  "settings.gridContainer": "Cuadrícula sin color",
  "settings.coloredGridContainer": "Cuadrícula coloreada"
  "settings.coloredGridContainer": "Cuadrícula coloreada",
  "appNote.edit": "Editar nota de uso…",
  "appNote.placeholder": "Nota de uso de la app",
  "edit.modeAdd": "Anadir etiquetas",
  "edit.modeRemove": "Quitar etiquetas",
  "edit.modeHint": "Las marcas de la izquierda solo se aplican a esta operacion.",
  "edit.currentTags": "Etiquetas actuales",
  "edit.commonTags": "Etiquetas compartidas",
  "edit.selectionHint": "Seleccione aplicaciones a la derecha y luego elija a la izquierda las etiquetas que desea anadir o quitar.",
  "edit.noAssignedTags": "La aplicacion seleccionada aun no tiene etiquetas.",
  "edit.noCommonTags": "Las aplicaciones seleccionadas no tienen etiquetas en comun.",
  "edit.mixedTagsHint": "Las demas etiquetas difieren entre las aplicaciones seleccionadas.",
  "edit.operationLabel": "Accion:",
  "edit.confirmAdd": "Confirmar agregado",
  "edit.confirmRemove": "Confirmar eliminacion",
  "edit.feedbackAddFormat": "Esta operacion agrego %tagCount% etiquetas a %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Esta operacion elimino %tagCount% etiquetas de %appCount% apps (%appNames%). Etiquetas eliminadas: %tagNames%.",
  "edit.removeHint": "Aqui solo se pueden desmarcar etiquetas ya existentes. Las etiquetas con icono rojo de eliminacion se quitaran tras confirmar."
}
Apptag/Localization/fr.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Échec de l'export",
  "settings.import": "Importer catégories et disposition…",
  "settings.backupDesc": "L'export sauvegarde les catégories et la disposition dans un fichier JSON. L'import remplace la configuration actuelle.",
  "settings.uncommonBubble": "Bulles pour apps peu utilisées",
  "settings.uncommonBubbleDesc": "Affiche une grande bulle avec le nom et une note uniquement pour les apps marquées comme peu utilisées.",
  "settings.importFailed": "Échec de l'import",
  "edit.exit": "Quitter l'édition",
  "edit.tags": "Modifier les étiquettes",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Non classé",
  "group.appleBuiltIn": "Intégrées",
  "group.uncommon": "Peu utilisé",
  "drop.systemDefaultWarning": "Ne déposez pas dans les catégories système par défaut",
  "tag.design": "Design",
  "tag.development": "Développement",
@@ -64,5 +67,22 @@
  "tag.productivity": "Bureau",
  "settings.coloredContainer": "Conteneur coloré",
  "settings.gridContainer": "Grille sans couleur",
  "settings.coloredGridContainer": "Grille colorée"
  "settings.coloredGridContainer": "Grille colorée",
  "appNote.edit": "Modifier la note d’usage…",
  "appNote.placeholder": "Note d’usage de l’app",
  "edit.modeAdd": "Ajouter des etiquettes",
  "edit.modeRemove": "Retirer des etiquettes",
  "edit.modeHint": "Les coches a gauche ne s'appliquent qu'a cette operation.",
  "edit.currentTags": "Etiquettes actuelles",
  "edit.commonTags": "Etiquettes communes",
  "edit.selectionHint": "Selectionnez des apps a droite, puis choisissez a gauche les etiquettes a ajouter ou a retirer.",
  "edit.noAssignedTags": "L'app selectionnee n'a pas encore d'etiquette.",
  "edit.noCommonTags": "Les apps selectionnees n'ont aucune etiquette commune.",
  "edit.mixedTagsHint": "Les autres etiquettes different selon les apps selectionnees.",
  "edit.operationLabel": "Action :",
  "edit.confirmAdd": "Confirmer ajout",
  "edit.confirmRemove": "Confirmer retrait",
  "edit.feedbackAddFormat": "Cette operation a ajoute %tagCount% etiquettes sur %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Cette operation a supprime %tagCount% etiquettes sur %appCount% apps (%appNames%). Etiquettes retirees : %tagNames%.",
  "edit.removeHint": "Ici, seuls les tags existants peuvent etre decoches. Les tags affichant une icone rouge de suppression seront retires apres confirmation."
}
Apptag/Localization/id.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Ekspor gagal",
  "settings.import": "Impor kategori & tata letak…",
  "settings.backupDesc": "Ekspor menyimpan kategori dan tata letak ke file JSON. Impor mengganti pengaturan saat ini dengan file.",
  "settings.uncommonBubble": "Gelembung app jarang dipakai",
  "settings.uncommonBubbleDesc": "Tampilkan gelembung nama besar dan catatan hanya untuk app yang ditandai jarang dipakai.",
  "settings.importFailed": "Impor gagal",
  "edit.exit": "Keluar dari pengeditan",
  "edit.tags": "Edit tag",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Tanpa kategori",
  "group.appleBuiltIn": "Bawaan",
  "group.uncommon": "Jarang dipakai",
  "drop.systemDefaultWarning": "Jangan seret app ke kategori bawaan sistem",
  "tag.design": "Desain",
  "tag.development": "Pengembangan",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitas",
  "settings.coloredContainer": "Kontainer berwarna",
  "settings.gridContainer": "Grid tanpa warna",
  "settings.coloredGridContainer": "Grid berwarna"
  "settings.coloredGridContainer": "Grid berwarna",
  "appNote.edit": "Edit catatan kegunaan…",
  "appNote.placeholder": "Catatan kegunaan app",
  "edit.modeAdd": "Tambahkan label",
  "edit.modeRemove": "Hapus label",
  "edit.modeHint": "Centang di kiri hanya berlaku untuk operasi ini.",
  "edit.currentTags": "Label saat ini",
  "edit.commonTags": "Label bersama",
  "edit.selectionHint": "Pilih app di kanan, lalu pilih di kiri label yang ingin ditambahkan atau dihapus.",
  "edit.noAssignedTags": "App yang dipilih belum memiliki label.",
  "edit.noCommonTags": "App yang dipilih tidak memiliki label bersama.",
  "edit.mixedTagsHint": "Label lainnya berbeda di antara app yang dipilih.",
  "edit.operationLabel": "Aksi:",
  "edit.confirmAdd": "Konfirmasi tambah",
  "edit.confirmRemove": "Konfirmasi hapus",
  "edit.feedbackAddFormat": "Operasi ini menambahkan %tagCount% tag ke %appCount% app (%appNames%).",
  "edit.feedbackRemoveFormat": "Operasi ini menghapus %tagCount% tag dari %appCount% app (%appNames%). Tag yang dihapus: %tagNames%.",
  "edit.removeHint": "Di sini hanya tag yang sudah ada yang bisa dihapus centangnya. Tag dengan ikon hapus merah akan dihapus setelah konfirmasi."
}
Apptag/Localization/it.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Esportazione fallita",
  "settings.import": "Importa categorie e layout…",
  "settings.backupDesc": "L'esportazione salva categorie e layout in un file JSON. L'importazione sostituisce la configurazione attuale.",
  "settings.uncommonBubble": "Bolle per app poco usate",
  "settings.uncommonBubbleDesc": "Mostra una grande bolla con nome e nota solo per le app segnate come poco usate.",
  "settings.importFailed": "Importazione fallita",
  "edit.exit": "Esci dalla modifica",
  "edit.tags": "Modifica tag",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Non categorizzato",
  "group.appleBuiltIn": "Integrate",
  "group.uncommon": "Poco usate",
  "drop.systemDefaultWarning": "Non trascinare nelle categorie di sistema predefinite",
  "tag.design": "Design",
  "tag.development": "Sviluppo",
@@ -64,5 +67,22 @@
  "tag.productivity": "Ufficio",
  "settings.coloredContainer": "Contenitore colorato",
  "settings.gridContainer": "Griglia senza colore",
  "settings.coloredGridContainer": "Griglia colorata"
  "settings.coloredGridContainer": "Griglia colorata",
  "appNote.edit": "Modifica nota d’uso…",
  "appNote.placeholder": "Nota d’uso dell’app",
  "edit.modeAdd": "Aggiungi tag",
  "edit.modeRemove": "Rimuovi tag",
  "edit.modeHint": "I segni di spunta a sinistra valgono solo per questa operazione.",
  "edit.currentTags": "Tag attuali",
  "edit.commonTags": "Tag condivisi",
  "edit.selectionHint": "Seleziona le app a destra, poi scegli a sinistra i tag da aggiungere o rimuovere.",
  "edit.noAssignedTags": "L'app selezionata non ha ancora tag.",
  "edit.noCommonTags": "Le app selezionate non hanno tag in comune.",
  "edit.mixedTagsHint": "Gli altri tag variano tra le app selezionate.",
  "edit.operationLabel": "Azione:",
  "edit.confirmAdd": "Conferma aggiunta",
  "edit.confirmRemove": "Conferma rimozione",
  "edit.feedbackAddFormat": "Questa operazione ha aggiunto %tagCount% tag a %appCount% app (%appNames%).",
  "edit.feedbackRemoveFormat": "Questa operazione ha rimosso %tagCount% tag da %appCount% app (%appNames%). Tag rimossi: %tagNames%.",
  "edit.removeHint": "Qui puoi deselezionare solo i tag gia esistenti. I tag che mostrano un'icona rossa di eliminazione verranno rimossi dopo la conferma."
}
Apptag/Localization/ja.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "エクスポート失敗",
  "settings.import": "カテゴリとレイアウトをインポート…",
  "settings.backupDesc": "エクスポートはカテゴリとレイアウトをJSONファイルに保存します。インポートは現在の設定をファイルの内容で置き換えます。",
  "settings.uncommonBubble": "あまり使わないアプリの吹き出し",
  "settings.uncommonBubbleDesc": "あまり使わないとマークしたアプリだけに、大きな名前と用途メモを表示します。",
  "settings.importFailed": "インポート失敗",
  "edit.exit": "編集を終了",
  "edit.tags": "タグを編集",
@@ -54,6 +56,7 @@
  "app.build": "ビルド",
  "group.uncategorized": "未分類",
  "group.appleBuiltIn": "Mac内蔵",
  "group.uncommon": "あまり使わない",
  "drop.systemDefaultWarning": "システム既定カテゴリにはドラッグできません",
  "tag.design": "デザイン",
  "tag.development": "プログラミング",
@@ -64,5 +67,22 @@
  "tag.productivity": "オフィス",
  "settings.coloredContainer": "カラーコンテナ",
  "settings.gridContainer": "無色グリッド",
  "settings.coloredGridContainer": "カラーグリッド"
  "settings.coloredGridContainer": "カラーグリッド",
  "appNote.edit": "用途メモを編集…",
  "appNote.placeholder": "アプリの用途メモ",
  "edit.modeAdd": "タグを追加",
  "edit.modeRemove": "タグを削除",
  "edit.modeHint": "左側のチェックは今回の操作にのみ適用されます。",
  "edit.currentTags": "現在のタグ",
  "edit.commonTags": "共通タグ",
  "edit.selectionHint": "右側でアプリを選択し、左側で今回追加または削除するタグを選んでください。",
  "edit.noAssignedTags": "選択したアプリにはまだタグがありません。",
  "edit.noCommonTags": "選択したアプリに共通するタグはありません。",
  "edit.mixedTagsHint": "そのほかのタグは選択したアプリごとに異なります。",
  "edit.operationLabel": "今回の操作:",
  "edit.confirmAdd": "追加を確認",
  "edit.confirmRemove": "削除を確認",
  "edit.feedbackAddFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)に%tagCount%個のタグを追加しました。",
  "edit.feedbackRemoveFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)から%tagCount%個のタグを削除しました。削除したタグ: %tagNames%。",
  "edit.removeHint": "ここでは既存のタグだけチェックを外せます。赤い削除アイコンが表示されたタグは、確認後に削除されます。"
}
Apptag/Localization/ko.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "내보내기 실패",
  "settings.import": "분류와 레이아웃 가져오기…",
  "settings.backupDesc": "내보내기는 분류와 레이아웃을 JSON 파일로 저장합니다. 가져오기는 파일의 내용으로 현재 설정을 대체합니다.",
  "settings.uncommonBubble": "자주 쓰지 않는 앱 말풍선",
  "settings.uncommonBubbleDesc": "자주 쓰지 않음으로 표시한 앱에만 큰 이름과 메모 말풍선을 표시합니다.",
  "settings.importFailed": "가져오기 실패",
  "edit.exit": "편집 종료",
  "edit.tags": "태그 편집",
@@ -54,6 +56,7 @@
  "app.build": "빌드",
  "group.uncategorized": "미분류",
  "group.appleBuiltIn": "Mac 내장",
  "group.uncommon": "자주 쓰지 않음",
  "drop.systemDefaultWarning": "시스템 기본 분류로 드래그하지 마세요",
  "tag.design": "디자인",
  "tag.development": "프로그래밍",
@@ -64,5 +67,22 @@
  "tag.productivity": "오피스",
  "settings.coloredContainer": "색상 컨테이너",
  "settings.gridContainer": "무색 그리드",
  "settings.coloredGridContainer": "색상 그리드"
  "settings.coloredGridContainer": "색상 그리드",
  "appNote.edit": "용도 메모 편집…",
  "appNote.placeholder": "앱 용도 메모",
  "edit.modeAdd": "태그 추가",
  "edit.modeRemove": "태그 제거",
  "edit.modeHint": "왼쪽 체크는 이번 작업에만 적용됩니다.",
  "edit.currentTags": "현재 태그",
  "edit.commonTags": "공통 태그",
  "edit.selectionHint": "오른쪽에서 앱을 선택한 뒤, 왼쪽에서 이번에 추가하거나 제거할 태그를 고르세요.",
  "edit.noAssignedTags": "선택한 앱에는 아직 태그가 없습니다.",
  "edit.noCommonTags": "선택한 앱들 사이에 공통 태그가 없습니다.",
  "edit.mixedTagsHint": "다른 태그는 선택한 앱마다 서로 다릅니다.",
  "edit.operationLabel": "이번 작업:",
  "edit.confirmAdd": "추가 확인",
  "edit.confirmRemove": "제거 확인",
  "edit.feedbackAddFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에 태그 %tagCount%개가 새로 추가되었습니다.",
  "edit.feedbackRemoveFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에서 태그 %tagCount%개가 제거되었습니다. 제거된 태그: %tagNames%.",
  "edit.removeHint": "여기서는 이미 있는 태그만 체크 해제할 수 있습니다. 빨간 삭제 아이콘이 보이는 태그는 확인 후 제거됩니다."
}
Apptag/Localization/ms.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Ekspor gagal",
  "settings.import": "Import kategori & susun atur…",
  "settings.backupDesc": "Eksport menyimpan kategori dan susun atur ke fail JSON. Import menggantikan tetapan semasa dengan fail.",
  "settings.uncommonBubble": "Gelembung app jarang digunakan",
  "settings.uncommonBubbleDesc": "Tunjukkan gelembung nama besar dan nota hanya untuk app yang ditanda jarang digunakan.",
  "settings.importFailed": "Impor gagal",
  "edit.exit": "Keluar dari pengeditan",
  "edit.tags": "Edit tag",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Tidak berkategori",
  "group.appleBuiltIn": "Terbina dalam",
  "group.uncommon": "Jarang digunakan",
  "drop.systemDefaultWarning": "Jangan seret app ke kategori bawaan sistem",
  "tag.design": "Desain",
  "tag.development": "Pengembangan",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitas",
  "settings.coloredContainer": "Kontainer berwarna",
  "settings.gridContainer": "Grid tanpa warna",
  "settings.coloredGridContainer": "Grid berwarna"
  "settings.coloredGridContainer": "Grid berwarna",
  "appNote.edit": "Edit nota kegunaan…",
  "appNote.placeholder": "Nota kegunaan app",
  "edit.modeAdd": "Tambah tag",
  "edit.modeRemove": "Buang tag",
  "edit.modeHint": "Tanda di sebelah kiri hanya digunakan untuk operasi ini.",
  "edit.currentTags": "Tag semasa",
  "edit.commonTags": "Tag bersama",
  "edit.selectionHint": "Pilih app di sebelah kanan, kemudian pilih di sebelah kiri tag yang mahu ditambah atau dibuang.",
  "edit.noAssignedTags": "App yang dipilih belum mempunyai tag.",
  "edit.noCommonTags": "App yang dipilih tidak mempunyai tag bersama.",
  "edit.mixedTagsHint": "Tag lain berbeza antara app yang dipilih.",
  "edit.operationLabel": "Tindakan:",
  "edit.confirmAdd": "Sahkan tambah",
  "edit.confirmRemove": "Sahkan buang",
  "edit.feedbackAddFormat": "Operasi ini menambah %tagCount% tag pada %appCount% app (%appNames%).",
  "edit.feedbackRemoveFormat": "Operasi ini membuang %tagCount% tag daripada %appCount% app (%appNames%). Tag yang dibuang: %tagNames%.",
  "edit.removeHint": "Di sini hanya tag yang sedia ada boleh dinyah tanda. Tag yang memaparkan ikon padam merah akan dibuang selepas pengesahan."
}
Apptag/Localization/nb.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Eksport mislyktes",
  "settings.import": "Importer kategorier og layout…",
  "settings.backupDesc": "Eksport lagrer kategorier og layout i en JSON-fil. Import erstatter gjeldende oppsett med en fil.",
  "settings.uncommonBubble": "Bobler for lite brukte apper",
  "settings.uncommonBubbleDesc": "Viser stor navn- og notatboble bare for apper merket som lite brukt.",
  "settings.importFailed": "Import mislyktes",
  "edit.exit": "Avslutt redigering",
  "edit.tags": "Rediger etiketter",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Ukategorisert",
  "group.appleBuiltIn": "Innebygd",
  "group.uncommon": "Lite brukt",
  "drop.systemDefaultWarning": "Ikke dra apper til systemets standardkategorier",
  "tag.design": "Design",
  "tag.development": "Utvikling",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitet",
  "settings.coloredContainer": "Farget beholder",
  "settings.gridContainer": "Fargeløst rutenett",
  "settings.coloredGridContainer": "Farget rutenett"
  "settings.coloredGridContainer": "Farget rutenett",
  "appNote.edit": "Rediger bruksnotat…",
  "appNote.placeholder": "Bruksnotat for app",
  "edit.modeAdd": "Legg til tagger",
  "edit.modeRemove": "Fjern tagger",
  "edit.modeHint": "Avkrysningene til venstre gjelder bare for denne handlingen.",
  "edit.currentTags": "Gjeldende tagger",
  "edit.commonTags": "Felles tagger",
  "edit.selectionHint": "Velg apper til hoyre, og velg deretter til venstre hvilke tagger som skal legges til eller fjernes.",
  "edit.noAssignedTags": "Den valgte appen har ingen tagger ennå.",
  "edit.noCommonTags": "De valgte appene har ingen felles tagger.",
  "edit.mixedTagsHint": "Andre tagger varierer mellom de valgte appene.",
  "edit.operationLabel": "Handling:",
  "edit.confirmAdd": "Bekreft legg til",
  "edit.confirmRemove": "Bekreft fjern",
  "edit.feedbackAddFormat": "Denne handlingen la til %tagCount% tagger i %appCount% apper (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handlingen fjernet %tagCount% tagger fra %appCount% apper (%appNames%). Fjernede tagger: %tagNames%.",
  "edit.removeHint": "Her kan bare eksisterende tagger fjernes fra avkrysningen. Tagger med rodt sletteikon blir fjernet etter bekreftelse."
}
Apptag/Localization/nl.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Export mislukt",
  "settings.import": "Categorieën en lay-out importeren…",
  "settings.backupDesc": "Export slaat categorieën en lay-out op in een JSON-bestand. Import vervangt de huidige instelling door een bestand.",
  "settings.uncommonBubble": "Bubbels voor weinig gebruikte apps",
  "settings.uncommonBubbleDesc": "Toont een grote naam- en notitiebubbel alleen voor apps die als weinig gebruikt zijn gemarkeerd.",
  "settings.importFailed": "Import mislukt",
  "edit.exit": "Bewerken afsluiten",
  "edit.tags": "Tags bewerken",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Niet gecategoriseerd",
  "group.appleBuiltIn": "Ingebouwd",
  "group.uncommon": "Weinig gebruikt",
  "drop.systemDefaultWarning": "Sleep apps niet naar standaard systeemcategorieën",
  "tag.design": "Ontwerp",
  "tag.development": "Ontwikkeling",
@@ -64,5 +67,22 @@
  "tag.productivity": "Productiviteit",
  "settings.coloredContainer": "Gekleurde container",
  "settings.gridContainer": "Kleurloos raster",
  "settings.coloredGridContainer": "Gekleurd raster"
  "settings.coloredGridContainer": "Gekleurd raster",
  "appNote.edit": "Gebruiksnotitie bewerken…",
  "appNote.placeholder": "Gebruiksnotitie voor app",
  "edit.modeAdd": "Tags toevoegen",
  "edit.modeRemove": "Tags verwijderen",
  "edit.modeHint": "De vinkjes links gelden alleen voor deze bewerking.",
  "edit.currentTags": "Huidige tags",
  "edit.commonTags": "Gedeelde tags",
  "edit.selectionHint": "Selecteer rechts apps en kies links de tags die je wilt toevoegen of verwijderen.",
  "edit.noAssignedTags": "De geselecteerde app heeft nog geen tags.",
  "edit.noCommonTags": "De geselecteerde apps hebben geen gedeelde tags.",
  "edit.mixedTagsHint": "Andere tags verschillen per geselecteerde app.",
  "edit.operationLabel": "Actie:",
  "edit.confirmAdd": "Toevoegen bevestigen",
  "edit.confirmRemove": "Verwijderen bevestigen",
  "edit.feedbackAddFormat": "Deze actie heeft %tagCount% tags toegevoegd aan %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Deze actie heeft %tagCount% tags verwijderd van %appCount% apps (%appNames%). Verwijderde tags: %tagNames%.",
  "edit.removeHint": "Hier kun je alleen bestaande tags uitvinken. Tags met een rood verwijderpictogram worden na bevestiging verwijderd."
}
Apptag/Localization/nn.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Eksport mislyktes",
  "settings.import": "Importer kategorier og layout…",
  "settings.backupDesc": "Eksport lagrer kategorier og layout i en JSON-fil. Import erstatter gjeldende oppsett med en fil.",
  "settings.uncommonBubble": "Bobler for lite brukte appar",
  "settings.uncommonBubbleDesc": "Viser stor namn- og notatboble berre for appar merkte som lite brukt.",
  "settings.importFailed": "Import mislyktes",
  "edit.exit": "Avslutt redigering",
  "edit.tags": "Rediger etiketter",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Ukategorisert",
  "group.appleBuiltIn": "Innebygd",
  "group.uncommon": "Lite brukt",
  "drop.systemDefaultWarning": "Ikke dra apper til systemets standardkategorier",
  "tag.design": "Design",
  "tag.development": "Utvikling",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitet",
  "settings.coloredContainer": "Farget beholder",
  "settings.gridContainer": "Fargeløst rutenett",
  "settings.coloredGridContainer": "Farget rutenett"
  "settings.coloredGridContainer": "Farget rutenett",
  "appNote.edit": "Rediger bruksnotat…",
  "appNote.placeholder": "Bruksnotat for app",
  "edit.modeAdd": "Legg til taggar",
  "edit.modeRemove": "Fjern taggar",
  "edit.modeHint": "Avkryssingane til venstre gjeld berre for denne handlinga.",
  "edit.currentTags": "Noverande taggar",
  "edit.commonTags": "Felles taggar",
  "edit.selectionHint": "Vel appar til hogre, og vel deretter til venstre kva taggar som skal leggjast til eller fjernast.",
  "edit.noAssignedTags": "Den valde appen har ingen taggar enno.",
  "edit.noCommonTags": "Dei valde appane har ingen felles taggar.",
  "edit.mixedTagsHint": "Andre taggar varierer mellom dei valde appane.",
  "edit.operationLabel": "Handling:",
  "edit.confirmAdd": "Stadfest legg til",
  "edit.confirmRemove": "Stadfest fjern",
  "edit.feedbackAddFormat": "Denne handlinga la til %tagCount% taggar i %appCount% appar (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handlinga fjerna %tagCount% taggar fra %appCount% appar (%appNames%). Fjerna taggar: %tagNames%.",
  "edit.removeHint": "Her kan berre eksisterande taggar fjernast frå avkryssinga. Taggar med raudt sletteikon blir fjerna etter stadfesting."
}
Apptag/Localization/no.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Eksport mislyktes",
  "settings.import": "Importer kategorier og layout…",
  "settings.backupDesc": "Eksport lagrer kategorier og layout i en JSON-fil. Import erstatter gjeldende oppsett med en fil.",
  "settings.uncommonBubble": "Bobler for lite brukte apper",
  "settings.uncommonBubbleDesc": "Viser stor navn- og notatboble bare for apper merket som lite brukt.",
  "settings.importFailed": "Import mislyktes",
  "edit.exit": "Avslutt redigering",
  "edit.tags": "Rediger etiketter",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Ukategorisert",
  "group.appleBuiltIn": "Innebygd",
  "group.uncommon": "Lite brukt",
  "drop.systemDefaultWarning": "Ikke dra apper til systemets standardkategorier",
  "tag.design": "Design",
  "tag.development": "Utvikling",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitet",
  "settings.coloredContainer": "Farget beholder",
  "settings.gridContainer": "Fargeløst rutenett",
  "settings.coloredGridContainer": "Farget rutenett"
  "settings.coloredGridContainer": "Farget rutenett",
  "appNote.edit": "Rediger bruksnotat…",
  "appNote.placeholder": "Bruksnotat for app",
  "edit.modeAdd": "Legg til tagger",
  "edit.modeRemove": "Fjern tagger",
  "edit.modeHint": "Avkrysningene til venstre gjelder bare for denne handlingen.",
  "edit.currentTags": "Gjeldende tagger",
  "edit.commonTags": "Felles tagger",
  "edit.selectionHint": "Velg apper til hoyre, og velg deretter til venstre hvilke tagger som skal legges til eller fjernes.",
  "edit.noAssignedTags": "Den valgte appen har ingen tagger ennå.",
  "edit.noCommonTags": "De valgte appene har ingen felles tagger.",
  "edit.mixedTagsHint": "Andre tagger varierer mellom de valgte appene.",
  "edit.operationLabel": "Handling:",
  "edit.confirmAdd": "Bekreft legg til",
  "edit.confirmRemove": "Bekreft fjern",
  "edit.feedbackAddFormat": "Denne handlingen la til %tagCount% tagger i %appCount% apper (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handlingen fjernet %tagCount% tagger fra %appCount% apper (%appNames%). Fjernede tagger: %tagNames%.",
  "edit.removeHint": "Her kan bare eksisterende tagger fjernes fra avkrysningen. Tagger med rodt sletteikon blir fjernet etter bekreftelse."
}
Apptag/Localization/pl.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Eksport nieudany",
  "settings.import": "Importuj kategorie i układ…",
  "settings.backupDesc": "Eksport zapisuje kategorie i układ do pliku JSON. Import zastępuje bieżącą konfigurację plikiem.",
  "settings.uncommonBubble": "Dymki dla rzadko używanych aplikacji",
  "settings.uncommonBubbleDesc": "Pokazuje dużą nazwę i notatkę tylko dla aplikacji oznaczonych jako rzadko używane.",
  "settings.importFailed": "Import nieudany",
  "edit.exit": "Zakończ edycję",
  "edit.tags": "Edytuj tagi",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Bez kategorii",
  "group.appleBuiltIn": "Wbudowane",
  "group.uncommon": "Rzadko używane",
  "drop.systemDefaultWarning": "Nie przeciągaj aplikacji do domyślnych kategorii systemu",
  "tag.design": "Projektowanie",
  "tag.development": "Programowanie",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktywność",
  "settings.coloredContainer": "Kolorowy kontener",
  "settings.gridContainer": "Bezbarwna siatka",
  "settings.coloredGridContainer": "Kolorowa siatka"
  "settings.coloredGridContainer": "Kolorowa siatka",
  "appNote.edit": "Edytuj notatkę o przeznaczeniu…",
  "appNote.placeholder": "Notatka o przeznaczeniu aplikacji",
  "edit.modeAdd": "Dodaj tagi",
  "edit.modeRemove": "Usun tagi",
  "edit.modeHint": "Zaznaczenia po lewej dotycza tylko tej operacji.",
  "edit.currentTags": "Biezace tagi",
  "edit.commonTags": "Wspolne tagi",
  "edit.selectionHint": "Wybierz aplikacje po prawej, a nastepnie po lewej wybierz tagi do dodania lub usuniecia.",
  "edit.noAssignedTags": "Wybrana aplikacja nie ma jeszcze tagow.",
  "edit.noCommonTags": "Wybrane aplikacje nie maja wspolnych tagow.",
  "edit.mixedTagsHint": "Pozostale tagi roznia sie miedzy wybranymi aplikacjami.",
  "edit.operationLabel": "Dzialanie:",
  "edit.confirmAdd": "Potwierdz dodanie",
  "edit.confirmRemove": "Potwierdz usuniecie",
  "edit.feedbackAddFormat": "Ta operacja dodala %tagCount% tagow do %appCount% aplikacji (%appNames%).",
  "edit.feedbackRemoveFormat": "Ta operacja usunela %tagCount% tagow z %appCount% aplikacji (%appNames%). Usuniete tagi: %tagNames%.",
  "edit.removeHint": "Tutaj mozna odznaczyc tylko istniejace tagi. Tagi z czerwona ikona usuniecia zostana usuniete po potwierdzeniu."
}
Apptag/Localization/pt-BR.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Falha ao exportar",
  "settings.import": "Importar categorias e layout…",
  "settings.backupDesc": "A exportação salva categorias e layout em um arquivo JSON. A importação substitui a configuração atual por um arquivo.",
  "settings.uncommonBubble": "Balões para apps pouco usados",
  "settings.uncommonBubbleDesc": "Mostra um balão grande com nome e nota apenas para apps marcados como pouco usados.",
  "settings.importFailed": "Falha ao importar",
  "edit.exit": "Sair da edição",
  "edit.tags": "Editar etiquetas",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Sem categoria",
  "group.appleBuiltIn": "Integrados",
  "group.uncommon": "Pouco usados",
  "drop.systemDefaultWarning": "Não arraste apps para categorias padrão do sistema",
  "tag.design": "Design",
  "tag.development": "Desenvolvimento",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produtividade",
  "settings.coloredContainer": "Contêiner colorido",
  "settings.gridContainer": "Grade sem cor",
  "settings.coloredGridContainer": "Grade colorida"
  "settings.coloredGridContainer": "Grade colorida",
  "appNote.edit": "Editar nota de uso…",
  "appNote.placeholder": "Nota de uso do app",
  "edit.modeAdd": "Adicionar tags",
  "edit.modeRemove": "Remover tags",
  "edit.modeHint": "As marcacoes a esquerda valem apenas para esta operacao.",
  "edit.currentTags": "Tags atuais",
  "edit.commonTags": "Tags em comum",
  "edit.selectionHint": "Selecione apps a direita e depois escolha a esquerda as tags para adicionar ou remover.",
  "edit.noAssignedTags": "O app selecionado ainda nao tem tags.",
  "edit.noCommonTags": "Os apps selecionados nao tem tags em comum.",
  "edit.mixedTagsHint": "As outras tags diferem entre os apps selecionados.",
  "edit.operationLabel": "Acao:",
  "edit.confirmAdd": "Confirmar adicao",
  "edit.confirmRemove": "Confirmar remocao",
  "edit.feedbackAddFormat": "Esta operacao adicionou %tagCount% tags em %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Esta operacao removeu %tagCount% tags de %appCount% apps (%appNames%). Tags removidas: %tagNames%.",
  "edit.removeHint": "Aqui so e possivel desmarcar tags ja existentes. As tags com icone vermelha de remocao serao removidas apos a confirmacao."
}
Apptag/Localization/ro.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Export eșuat",
  "settings.import": "Importă categorii și aspect…",
  "settings.backupDesc": "Exportul salvează categoriile și aspectul într-un fișier JSON. Importul înlocuiește configurația curentă cu un fișier.",
  "settings.uncommonBubble": "Bule pentru aplicații folosite rar",
  "settings.uncommonBubbleDesc": "Afișează o bulă mare cu nume și notă doar pentru aplicațiile marcate ca folosite rar.",
  "settings.importFailed": "Import eșuat",
  "edit.exit": "Ieși din editare",
  "edit.tags": "Editează etichete",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Necategorizat",
  "group.appleBuiltIn": "Integrat",
  "group.uncommon": "Folosit rar",
  "drop.systemDefaultWarning": "Nu trage aplicații în categoriile implicite ale sistemului",
  "tag.design": "Design",
  "tag.development": "Dezvoltare",
@@ -64,5 +67,22 @@
  "tag.productivity": "Productivitate",
  "settings.coloredContainer": "Container colorat",
  "settings.gridContainer": "Grilă fără culoare",
  "settings.coloredGridContainer": "Grilă colorată"
  "settings.coloredGridContainer": "Grilă colorată",
  "appNote.edit": "Editează nota de utilizare…",
  "appNote.placeholder": "Notă de utilizare a aplicației",
  "edit.modeAdd": "Adauga etichete",
  "edit.modeRemove": "Elimina etichete",
  "edit.modeHint": "Bifele din stanga se aplica doar acestei operatii.",
  "edit.currentTags": "Etichete curente",
  "edit.commonTags": "Etichete comune",
  "edit.selectionHint": "Selecteaza aplicatii in dreapta, apoi alege in stanga etichetele pe care vrei sa le adaugi sau sa le elimini.",
  "edit.noAssignedTags": "Aplicatia selectata nu are inca etichete.",
  "edit.noCommonTags": "Aplicatiile selectate nu au etichete comune.",
  "edit.mixedTagsHint": "Celelalte etichete difera intre aplicatiile selectate.",
  "edit.operationLabel": "Actiune:",
  "edit.confirmAdd": "Confirma adaugarea",
  "edit.confirmRemove": "Confirma eliminarea",
  "edit.feedbackAddFormat": "Aceasta actiune a adaugat %tagCount% etichete in %appCount% aplicatii (%appNames%).",
  "edit.feedbackRemoveFormat": "Aceasta actiune a eliminat %tagCount% etichete din %appCount% aplicatii (%appNames%). Etichete eliminate: %tagNames%.",
  "edit.removeHint": "Aici pot fi debifate doar etichetele deja existente. Etichetele care afiseaza o pictograma rosie de stergere vor fi eliminate dupa confirmare."
}
Apptag/Localization/ru.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Ошибка экспорта",
  "settings.import": "Импорт категорий и макета…",
  "settings.backupDesc": "Экспорт сохраняет категории и макет в JSON-файл. Импорт заменяет текущие настройки данными из файла.",
  "settings.uncommonBubble": "Подсказки для редких приложений",
  "settings.uncommonBubbleDesc": "Показывает большое имя и заметку только для приложений, отмеченных как редко используемые.",
  "settings.importFailed": "Ошибка импорта",
  "edit.exit": "Выйти из редактирования",
  "edit.tags": "Редактировать теги",
@@ -54,6 +56,7 @@
  "app.build": "Сборка",
  "group.uncategorized": "Без категории",
  "group.appleBuiltIn": "Встроенные",
  "group.uncommon": "Редко используется",
  "drop.systemDefaultWarning": "Не перетаскивайте в системные категории по умолчанию",
  "tag.design": "Дизайн",
  "tag.development": "Разработка",
@@ -64,5 +67,22 @@
  "tag.productivity": "Офис",
  "settings.coloredContainer": "Цветной контейнер",
  "settings.gridContainer": "Бесцветная сетка",
  "settings.coloredGridContainer": "Цветная сетка"
  "settings.coloredGridContainer": "Цветная сетка",
  "appNote.edit": "Изменить заметку о назначении…",
  "appNote.placeholder": "Заметка о назначении приложения",
  "edit.modeAdd": "Добавить теги",
  "edit.modeRemove": "Удалить теги",
  "edit.modeHint": "Отметки слева применяются только к этой операции.",
  "edit.currentTags": "Текущие теги",
  "edit.commonTags": "Общие теги",
  "edit.selectionHint": "Выберите приложения справа, затем слева отметьте теги, которые нужно добавить или удалить.",
  "edit.noAssignedTags": "У выбранного приложения пока нет тегов.",
  "edit.noCommonTags": "У выбранных приложений нет общих тегов.",
  "edit.mixedTagsHint": "Остальные теги различаются у выбранных приложений.",
  "edit.operationLabel": "Действие:",
  "edit.confirmAdd": "Подтвердить добавление",
  "edit.confirmRemove": "Подтвердить удаление",
  "edit.feedbackAddFormat": "Эта операция добавила %tagCount% тегов для %appCount% приложений (%appNames%).",
  "edit.feedbackRemoveFormat": "Эта операция удалила %tagCount% тегов у %appCount% приложений (%appNames%). Удаленные теги: %tagNames%.",
  "edit.removeHint": "Здесь можно снять отметку только с уже существующих тегов. Теги с красным значком удаления будут удалены после подтверждения."
}
Apptag/Localization/sr-Cyrl.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Извоз није успео",
  "settings.import": "Увези категорије и распоред…",
  "settings.backupDesc": "Извоз чува категорије и распоред у JSON датотеку. Увоз замењује тренутна подешавања датотеком.",
  "settings.uncommonBubble": "Облачићи за ретке апликације",
  "settings.uncommonBubbleDesc": "Приказује велико име и белешку само за апликације означене као ретко коришћене.",
  "settings.importFailed": "Увоз није успео",
  "edit.exit": "Изађи из уређивања",
  "edit.tags": "Уреди ознаке",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Некатегорисано",
  "group.appleBuiltIn": "Уграђено",
  "group.uncommon": "Ретко коришћено",
  "drop.systemDefaultWarning": "Не превлачите апликације у подразумеване системске категорије",
  "tag.design": "Дизајн",
  "tag.development": "Развој",
@@ -64,5 +67,22 @@
  "tag.productivity": "Продуктивност",
  "settings.coloredContainer": "Обојени контејнер",
  "settings.gridContainer": "Безбојна мрежа",
  "settings.coloredGridContainer": "Обојена мрежа"
  "settings.coloredGridContainer": "Обојена мрежа",
  "appNote.edit": "Уреди белешку о намени…",
  "appNote.placeholder": "Белешка о намени апликације",
  "edit.modeAdd": "Додај ознаке",
  "edit.modeRemove": "Уклони ознаке",
  "edit.modeHint": "Означавање лево важи само за ову радњу.",
  "edit.currentTags": "Тренутне ознаке",
  "edit.commonTags": "Заједничке ознаке",
  "edit.selectionHint": "Изаберите апликације десно, па лево изаберите ознаке које желите да додате или уклоните.",
  "edit.noAssignedTags": "Изабрана апликација још нема ознаке.",
  "edit.noCommonTags": "Изабране апликације немају заједничке ознаке.",
  "edit.mixedTagsHint": "Остале ознаке се разликују између изабраних апликација.",
  "edit.operationLabel": "Радња:",
  "edit.confirmAdd": "Потврди додавање",
  "edit.confirmRemove": "Потврди уклањање",
  "edit.feedbackAddFormat": "Ова радња је додала %tagCount% ознака за %appCount% апликација (%appNames%).",
  "edit.feedbackRemoveFormat": "Ова радња је уклонила %tagCount% ознака са %appCount% апликација (%appNames%). Уклоњене ознаке: %tagNames%.",
  "edit.removeHint": "Овде можете одчекирати само постојеће ознаке. Ознаке са црвеном иконицом брисања биће уклоњене након потврде."
}
Apptag/Localization/sv.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Export misslyckades",
  "settings.import": "Importera kategorier och layout…",
  "settings.backupDesc": "Export sparar kategorier och layout till en JSON-fil. Import ersätter den aktuella konfigurationen med en fil.",
  "settings.uncommonBubble": "Bubblor för sällan använda appar",
  "settings.uncommonBubbleDesc": "Visar en stor namn- och anteckningsbubbla endast för appar markerade som sällan använda.",
  "settings.importFailed": "Import misslyckades",
  "edit.exit": "Avsluta redigering",
  "edit.tags": "Redigera taggar",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Okategoriserat",
  "group.appleBuiltIn": "Inbyggt",
  "group.uncommon": "Sällan använd",
  "drop.systemDefaultWarning": "Dra inte appar till systemets standardkategorier",
  "tag.design": "Design",
  "tag.development": "Utveckling",
@@ -64,5 +67,22 @@
  "tag.productivity": "Produktivitet",
  "settings.coloredContainer": "Färgad behållare",
  "settings.gridContainer": "Färglöst rutnät",
  "settings.coloredGridContainer": "Färgat rutnät"
  "settings.coloredGridContainer": "Färgat rutnät",
  "appNote.edit": "Redigera användningsanteckning…",
  "appNote.placeholder": "Användningsanteckning för app",
  "edit.modeAdd": "Lagg till taggar",
  "edit.modeRemove": "Ta bort taggar",
  "edit.modeHint": "Markeringarna till vanster galler bara for den har atgarden.",
  "edit.currentTags": "Nuvarande taggar",
  "edit.commonTags": "Gemensamma taggar",
  "edit.selectionHint": "Valj appar till hoger och valj sedan till vanster vilka taggar som ska laggas till eller tas bort.",
  "edit.noAssignedTags": "Den valda appen har inga taggar an.",
  "edit.noCommonTags": "De valda apparna har inga gemensamma taggar.",
  "edit.mixedTagsHint": "Ovriga taggar skiljer sig mellan de valda apparna.",
  "edit.operationLabel": "Atgard:",
  "edit.confirmAdd": "Bekrafta lagg till",
  "edit.confirmRemove": "Bekrafta ta bort",
  "edit.feedbackAddFormat": "Den har atgarden lade till %tagCount% taggar i %appCount% appar (%appNames%).",
  "edit.feedbackRemoveFormat": "Den har atgarden tog bort %tagCount% taggar fran %appCount% appar (%appNames%). Borttagna taggar: %tagNames%.",
  "edit.removeHint": "Har kan bara befintliga taggar avmarkeras. Taggar med en rod raderingsikon tas bort efter bekraftelse."
}
Apptag/Localization/th.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "ส่งออกไม่สำเร็จ",
  "settings.import": "นำเข้าหมวดหมู่และเลย์เอาต์…",
  "settings.backupDesc": "การส่งออกจะบันทึกหมวดหมู่และเลย์เอาต์เป็นไฟล์ JSON การนำเข้าจะแทนที่การตั้งค่าปัจจุบันด้วยไฟล์",
  "settings.uncommonBubble": "บับเบิลสำหรับแอปที่ไม่ค่อยใช้",
  "settings.uncommonBubbleDesc": "แสดงชื่อขนาดใหญ่และบันทึกเฉพาะแอปที่ทำเครื่องหมายว่าไม่ค่อยใช้",
  "settings.importFailed": "นำเข้าไม่สำเร็จ",
  "edit.exit": "ออกจากการแก้ไข",
  "edit.tags": "แก้ไขแท็ก",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "ไม่จัดหมวดหมู่",
  "group.appleBuiltIn": "ในตัว",
  "group.uncommon": "ไม่ค่อยใช้",
  "drop.systemDefaultWarning": "อย่าลากแอปไปยังหมวดหมู่เริ่มต้นของระบบ",
  "tag.design": "ออกแบบ",
  "tag.development": "พัฒนา",
@@ -64,5 +67,22 @@
  "tag.productivity": "งาน",
  "settings.coloredContainer": "คอนเทนเนอร์มีสี",
  "settings.gridContainer": "กริดไม่มีสี",
  "settings.coloredGridContainer": "กริดมีสี"
  "settings.coloredGridContainer": "กริดมีสี",
  "appNote.edit": "แก้ไขบันทึกการใช้งาน…",
  "appNote.placeholder": "บันทึกการใช้งานแอป",
  "edit.modeAdd": "เพิ่มแท็ก",
  "edit.modeRemove": "ลบแท็ก",
  "edit.modeHint": "เครื่องหมายทางซ้ายมีผลกับการทำงานครั้งนี้เท่านั้น",
  "edit.currentTags": "แท็กปัจจุบัน",
  "edit.commonTags": "แท็กร่วม",
  "edit.selectionHint": "เลือกแอปทางขวา แล้วเลือกทางซ้ายว่าแท็กใดจะเพิ่มหรือลบในการทำครั้งนี้",
  "edit.noAssignedTags": "แอปที่เลือกยังไม่มีแท็ก",
  "edit.noCommonTags": "แอปที่เลือกไม่มีแท็กร่วมกัน",
  "edit.mixedTagsHint": "แท็กอื่น ๆ ของแอปที่เลือกยังแตกต่างกัน",
  "edit.operationLabel": "การทำงานนี้:",
  "edit.confirmAdd": "ยืนยันการเพิ่ม",
  "edit.confirmRemove": "ยืนยันการลบ",
  "edit.feedbackAddFormat": "การทำงานครั้งนี้ได้เพิ่มแท็ก %tagCount% รายการให้กับแอป %appCount% แอป (%appNames%).",
  "edit.feedbackRemoveFormat": "การทำงานครั้งนี้ได้ลบแท็ก %tagCount% รายการออกจากแอป %appCount% แอป (%appNames%). แท็กที่ลบ: %tagNames%.",
  "edit.removeHint": "ที่นี่สามารถยกเลิกเครื่องหมายได้เฉพาะแท็กที่มีอยู่แล้วเท่านั้น แท็กที่แสดงไอคอนลบสีแดงจะถูกลบหลังจากยืนยัน"
}
Apptag/Localization/tr.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Dışa aktarma başarısız",
  "settings.import": "Kategorileri ve düzeni içe aktar…",
  "settings.backupDesc": "Dışa aktarma kategorileri ve düzeni JSON dosyasına kaydeder. İçe aktarma mevcut kurulumu dosyadakiyle değiştirir.",
  "settings.uncommonBubble": "Az kullanılan app balonları",
  "settings.uncommonBubbleDesc": "Yalnızca az kullanılan olarak işaretlenen app’lerde büyük ad ve not balonu gösterir.",
  "settings.importFailed": "İçe aktarma başarısız",
  "edit.exit": "Düzenlemeden çık",
  "edit.tags": "Etiketleri düzenle",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Kategorisiz",
  "group.appleBuiltIn": "Yerleşik",
  "group.uncommon": "Az kullanılan",
  "drop.systemDefaultWarning": "Uygulamaları sistem varsayılan kategorilerine sürüklemeyin",
  "tag.design": "Tasarım",
  "tag.development": "Geliştirme",
@@ -64,5 +67,22 @@
  "tag.productivity": "Verimlilik",
  "settings.coloredContainer": "Renkli Kapsayıcı",
  "settings.gridContainer": "Renksiz Izgara",
  "settings.coloredGridContainer": "Renkli Izgara"
  "settings.coloredGridContainer": "Renkli Izgara",
  "appNote.edit": "Kullanım notunu düzenle…",
  "appNote.placeholder": "App kullanım notu",
  "edit.modeAdd": "Etiket ekle",
  "edit.modeRemove": "Etiket kaldir",
  "edit.modeHint": "Soldaki isaretler yalnizca bu islem icin gecerlidir.",
  "edit.currentTags": "Guncel etiketler",
  "edit.commonTags": "Ortak etiketler",
  "edit.selectionHint": "Saga taraftan uygulamalari secin, sonra soldan eklemek veya kaldirmak istediginiz etiketleri secin.",
  "edit.noAssignedTags": "Secilen uygulamanin henuz etiketi yok.",
  "edit.noCommonTags": "Secilen uygulamalarin ortak etiketi yok.",
  "edit.mixedTagsHint": "Diger etiketler secilen uygulamalar arasinda farklilik gosterir.",
  "edit.operationLabel": "Islem:",
  "edit.confirmAdd": "Eklemeyi onayla",
  "edit.confirmRemove": "Kaldirmayi onayla",
  "edit.feedbackAddFormat": "Bu islem, %appCount% uygulamada (%appNames%) %tagCount% etiket ekledi.",
  "edit.feedbackRemoveFormat": "Bu islem, %appCount% uygulamadan (%appNames%) %tagCount% etiketi kaldirdi. Kaldirilan etiketler: %tagNames%.",
  "edit.removeHint": "Burada sadece mevcut etiketlerin isareti kaldirilabilir. Kirmizi silme simgesi gorunen etiketler onaydan sonra kaldirilir."
}
Apptag/Localization/uk.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Помилка експорту",
  "settings.import": "Імпорт категорій і макета…",
  "settings.backupDesc": "Експорт зберігає категорії та макет у JSON-файл. Імпорт замінює поточні налаштування даними з файлу.",
  "settings.uncommonBubble": "Підказки для рідкісних програм",
  "settings.uncommonBubbleDesc": "Показує велику назву й нотатку лише для програм, позначених як рідко використовувані.",
  "settings.importFailed": "Помилка імпорту",
  "edit.exit": "Вийти з редагування",
  "edit.tags": "Редагувати теги",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Без категорії",
  "group.appleBuiltIn": "Вбудовані",
  "group.uncommon": "Рідко використовується",
  "drop.systemDefaultWarning": "Не перетягуйте застосунки в системні категорії за замовчуванням",
  "tag.design": "Дизайн",
  "tag.development": "Розробка",
@@ -64,5 +67,22 @@
  "tag.productivity": "Продуктивність",
  "settings.coloredContainer": "Кольоровий контейнер",
  "settings.gridContainer": "Безбарвна сітка",
  "settings.coloredGridContainer": "Кольорова сітка"
  "settings.coloredGridContainer": "Кольорова сітка",
  "appNote.edit": "Редагувати нотатку про призначення…",
  "appNote.placeholder": "Нотатка про призначення програми",
  "edit.modeAdd": "Додати теги",
  "edit.modeRemove": "Вилучити теги",
  "edit.modeHint": "Позначки ліворуч застосовуються лише до цієї дії.",
  "edit.currentTags": "Поточні теги",
  "edit.commonTags": "Спільні теги",
  "edit.selectionHint": "Виберіть застосунки праворуч, а потім ліворуч позначте теги, які треба додати або вилучити.",
  "edit.noAssignedTags": "У вибраного застосунку ще немає тегів.",
  "edit.noCommonTags": "У вибраних застосунків немає спільних тегів.",
  "edit.mixedTagsHint": "Інші теги відрізняються між вибраними застосунками.",
  "edit.operationLabel": "Дія:",
  "edit.confirmAdd": "Підтвердити додавання",
  "edit.confirmRemove": "Підтвердити вилучення",
  "edit.feedbackAddFormat": "Ця дія додала %tagCount% тегів для %appCount% застосунків (%appNames%).",
  "edit.feedbackRemoveFormat": "Ця дія вилучила %tagCount% тегів у %appCount% застосунків (%appNames%). Вилучені теги: %tagNames%.",
  "edit.removeHint": "Тут можна зняти позначку лише з наявних тегів. Теги з червоною іконкою видалення буде вилучено після підтвердження."
}
Apptag/Localization/vi.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "Xuất thất bại",
  "settings.import": "Nhập danh mục & bố cục…",
  "settings.backupDesc": "Xuất sẽ lưu danh mục và bố cục thành tệp JSON. Nhập sẽ thay thế thiết lập hiện tại bằng một tệp.",
  "settings.uncommonBubble": "Bong bóng cho app ít dùng",
  "settings.uncommonBubbleDesc": "Chỉ hiển thị bong bóng tên lớn và ghi chú cho app được đánh dấu là ít dùng.",
  "settings.importFailed": "Nhập thất bại",
  "edit.exit": "Thoát chỉnh sửa",
  "edit.tags": "Sửa thẻ",
@@ -54,6 +56,7 @@
  "app.build": "Build",
  "group.uncategorized": "Chưa phân loại",
  "group.appleBuiltIn": "Tích hợp",
  "group.uncommon": "Ít dùng",
  "drop.systemDefaultWarning": "Không kéo ứng dụng vào danh mục mặc định của hệ thống",
  "tag.design": "Thiết kế",
  "tag.development": "Phát triển",
@@ -64,5 +67,22 @@
  "tag.productivity": "Năng suất",
  "settings.coloredContainer": "Vùng chứa có màu",
  "settings.gridContainer": "Lưới không màu",
  "settings.coloredGridContainer": "Lưới có màu"
  "settings.coloredGridContainer": "Lưới có màu",
  "appNote.edit": "Sửa ghi chú mục đích…",
  "appNote.placeholder": "Ghi chú mục đích của app",
  "edit.modeAdd": "Them nhan",
  "edit.modeRemove": "Xoa nhan",
  "edit.modeHint": "Cac dau tich ben trai chi ap dung cho thao tac nay.",
  "edit.currentTags": "Nhan hien tai",
  "edit.commonTags": "Nhan chung",
  "edit.selectionHint": "Chon app o ben phai, sau do chon o ben trai cac nhan can them hoac xoa.",
  "edit.noAssignedTags": "App da chon chua co nhan nao.",
  "edit.noCommonTags": "Cac app da chon khong co nhan chung.",
  "edit.mixedTagsHint": "Cac nhan khac khac nhau giua cac app da chon.",
  "edit.operationLabel": "Tac vu nay:",
  "edit.confirmAdd": "Xac nhan them",
  "edit.confirmRemove": "Xac nhan xoa",
  "edit.feedbackAddFormat": "Lan nay da them %tagCount% the cho %appCount% ung dung (%appNames%).",
  "edit.feedbackRemoveFormat": "Lan nay da go %tagCount% the khoi %appCount% ung dung (%appNames%). The da go: %tagNames%.",
  "edit.removeHint": "Tai day chi co the bo chon cac the da ton tai. Cac the hien bieu tuong xoa mau do se bi go sau khi xac nhan."
}
Apptag/Localization/zh-Hans.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "导出失败",
  "settings.import": "导入分类与布局…",
  "settings.backupDesc": "导出将分类与布局保存为 JSON 文件。导入用一个文件替换当前设置。",
  "settings.uncommonBubble": "不常用 App 气泡提示",
  "settings.uncommonBubbleDesc": "仅对标记为“不常用”的 App 显示大号名称与用途备忘。",
  "settings.importFailed": "导入失败",
  "edit.exit": "退出编辑",
  "edit.tags": "编辑标签",
@@ -54,6 +56,7 @@
  "app.build": "构建",
  "group.uncategorized": "未分类",
  "group.appleBuiltIn": "Mac自带",
  "group.uncommon": "不常用",
  "drop.systemDefaultWarning": "请勿拖动至系统默认分类",
  "tag.design": "设计",
  "tag.development": "编程",
@@ -64,5 +67,22 @@
  "tag.productivity": "办公",
  "settings.coloredContainer": "彩色容器",
  "settings.gridContainer": "无色网格",
  "settings.coloredGridContainer": "彩色网格"
  "settings.coloredGridContainer": "彩色网格",
  "appNote.edit": "编辑用途备忘…",
  "appNote.placeholder": "APP 用途备忘",
  "edit.modeAdd": "添加标签",
  "edit.modeRemove": "移除标签",
  "edit.modeHint": "左侧勾选只作用于本次操作。",
  "edit.currentTags": "当前标签",
  "edit.commonTags": "共同标签",
  "edit.selectionHint": "先在右侧选择应用,再在左侧勾选本次要添加或移除的标签。",
  "edit.noAssignedTags": "当前选中的应用还没有标签。",
  "edit.noCommonTags": "当前选中的这些应用没有共同标签。",
  "edit.mixedTagsHint": "这些应用的其他标签彼此不同。",
  "edit.operationLabel": "本轮操作:",
  "edit.confirmAdd": "确认添加",
  "edit.confirmRemove": "确认移除",
  "edit.feedbackAddFormat": "本次针对%appCount%个应用(%appNames%)新增了%tagCount%个标签。",
  "edit.feedbackRemoveFormat": "本次针对%appCount%个应用(%appNames%)移除了%tagCount%个标签(%tagNames%)。",
  "edit.removeHint": "仅可取消已有标签的勾选;出现红色删除图标的标签会在确认后被移除。"
}
Apptag/Localization/zh-Hant.json
@@ -33,6 +33,8 @@
  "settings.exportFailed": "匯出失敗",
  "settings.import": "匯入分類與版面配置…",
  "settings.backupDesc": "匯出將分類與版面配置儲存為 JSON 檔案。匯入用一個檔案取代目前設定。",
  "settings.uncommonBubble": "不常用 App 氣泡提示",
  "settings.uncommonBubbleDesc": "僅對標記為「不常用」的 App 顯示大號名稱與用途備忘。",
  "settings.importFailed": "匯入失敗",
  "edit.exit": "結束編輯",
  "edit.tags": "編輯標籤",
@@ -54,6 +56,7 @@
  "app.build": "建置",
  "group.uncategorized": "未分類",
  "group.appleBuiltIn": "Mac內建",
  "group.uncommon": "不常用",
  "drop.systemDefaultWarning": "請勿拖動至系統預設分類",
  "tag.design": "設計",
  "tag.development": "程式設計",
@@ -64,5 +67,22 @@
  "tag.productivity": "辦公",
  "settings.coloredContainer": "彩色容器",
  "settings.gridContainer": "無色網格",
  "settings.coloredGridContainer": "彩色網格"
  "settings.coloredGridContainer": "彩色網格",
  "appNote.edit": "編輯用途備忘…",
  "appNote.placeholder": "APP 用途備忘",
  "edit.modeAdd": "新增標籤",
  "edit.modeRemove": "移除標籤",
  "edit.modeHint": "左側勾選只作用於本次操作。",
  "edit.currentTags": "目前標籤",
  "edit.commonTags": "共同標籤",
  "edit.selectionHint": "先在右側選取應用,再在左側勾選本次要新增或移除的標籤。",
  "edit.noAssignedTags": "目前選取的應用尚未有標籤。",
  "edit.noCommonTags": "目前選取的這些應用沒有共同標籤。",
  "edit.mixedTagsHint": "這些應用的其他標籤彼此不同。",
  "edit.operationLabel": "本輪操作:",
  "edit.confirmAdd": "確認新增",
  "edit.confirmRemove": "確認移除",
  "edit.feedbackAddFormat": "本次針對%appCount%個應用(%appNames%)新增了%tagCount%個標籤。",
  "edit.feedbackRemoveFormat": "本次針對%appCount%個應用(%appNames%)移除了%tagCount%個標籤(%tagNames%)。",
  "edit.removeHint": "僅可取消既有標籤的勾選;出現紅色刪除圖示的標籤會在確認後被移除。"
}
Apptag/TagGroupView.swift
@@ -11,6 +11,8 @@
    var showNames: Bool = true
    var dragModeActive: Bool = false
    var onDragModeChange: ((Bool) -> Void)? = nil
    var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil
    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
    var onDropApp: ((String, String, Bool) -> Void)? = nil
    /// Adaptive columns — auto-fit based on icon size and available width.
@@ -53,6 +55,8 @@
                        sourceTag: group.name,
                        dragModeActive: dragModeActive,
                        onDragModeChange: onDragModeChange,
                        onBubbleHover: onBubbleHover,
                        onEditNote: onEditNote,
                        onSelect: { onSelectApp(app) }
                    )
                }
CHANGELOG.md
@@ -1,5 +1,36 @@
# Apptag Changelog
## [6.0.5] — 2026-05-16
- 修正应用分类编辑页的批量操作逻辑:添加模式只追加手动勾选的标签,移除模式会先自动勾选右侧已选 App 现有的标签合集,未命中的标签置灰不可点
- 移除模式下,取消某个已有标签的勾选后,会在标签右侧显示鲜红删除图标;重新勾回后,删除图标立即消失
- 优化移除模式的左侧提示文案,明确说明只有已有标签可取消勾选,出现红色删除图标的标签会在确认后被移除
- 新增居中全屏反馈提示:点击「确认添加 / 确认移除」后,会复用“不常用 App”气泡矩形样式显示本次操作结果,并支持自动消失和右上角关闭
- 反馈统计改为真实增减计数:逐个 App 计算实际新增/移除的标签关系数,避免把原本已存在或不存在的标签重复计入
- 调整编辑页顶部排版:将本轮操作说明移到确认按钮同一行,并为长文案提供省略与悬浮完整提示;左侧“选择标签 / 拖动排序”提示改为与标签列表左边缘对齐
- 将英文标题文案由 `This edit:` 收敛为更自然的 `Action:`,并同步检查、收紧其他语言的对应表述
- 修复编辑分类页左侧 sidebar 被说明文字横向撑开的布局问题,固定左栏宽度并恢复右侧应用区紧贴分隔线展开
- 左侧 `Select tags / Drag to reorder` 提示现在严格约束在标签列表宽度内,超长时换行,不再越出左侧标签列表边界
- 去掉编辑分类页顶部居中的标题,把空间让给右侧模式说明;右侧说明现在会优先尽量完整显示,只有左右按钮区确实排不下时才从中间省略
- 修复顶部模式说明使用无限宽度参与窗口约束计算时可能触发 AppKit 约束更新异常并闪退的问题
- 修复新版 macOS 上 Safari 等系统 App 可能被扫描遗漏的问题:扫描器现在会识别搜索目录的直接 `.app` 子级、补充 Cryptex 系统应用目录,并用真实路径去重但保留稳定入口路径
- 补齐编辑模式相关新增文案到全部语言文件,避免切换语言后出现缺失键
- 版本号更新为 `6.0.5`,Build 更新为 `610`
## [6.0.0] — 2026-05-16
- 新增“不常用”特殊系统标记:只在 App 分类编辑模式中显示,不作为普通分类出现在日常应用列表中
- “不常用”不改变 App 原有分类归属;App 仍留在原分类中,只额外获得低频标记
- 新增“不常用 App 气泡提示”开关,默认开启,放在设置页“数据”页签最上方
- 对已标记为“不常用”的 App,在开关开启后 hover 显示大号 App 名称 Bubble;有用途备注时同步显示备注
- 支持右键“不常用”App 编辑用途备忘,备注最多 80 个 Unicode 字符
- 备注数据进入分类与布局 JSON,并与“不常用”标记状态解耦:移除“不常用”标记后备注仍保留
- 编辑模式中的批量标签操作改为“添加标签 / 移除标签”双模式;右侧选中 App 不再自动反向勾选左侧标签,避免批量污染原有分类
- 为编辑模式新增只读标签状态区:单选显示当前标签,多选显示共同标签,并提示其他标签可能不同
- 新扫描到的 App 会自动标记为“不常用”,并开始统计从 TagLauncher 打开的次数;达到 100 次后,只有系统自动加上的“不常用”才会被自动移除
- 为新增功能补齐全部 29 个语言版本文案
- 版本号更新为 `6.0.0`,Build 更新为 `600`
## [5.7.8] — 2026-05-15
- 修复 TagLauncher 浮层每次打开都会重复添加 Escape 键本地监听的问题,关闭浮层时会同步释放监听器,避免事件监听累积